Wikilivres
frwikibooks
https://fr.wikibooks.org/wiki/Accueil
MediaWiki 1.45.0-wmf.8
first-letter
Média
Spécial
Discussion
Utilisateur
Discussion utilisateur
Wikilivres
Discussion Wikilivres
Fichier
Discussion fichier
MediaWiki
Discussion MediaWiki
Modèle
Discussion modèle
Aide
Discussion aide
Catégorie
Discussion catégorie
Transwiki
Discussion Transwiki
Wikijunior
Discussion Wikijunior
TimedText
TimedText talk
Module
Discussion module
Fonctionnement d'un ordinateur/Les processeurs superscalaires
0
65956
745702
745701
2025-07-02T12:06:33Z
Mewtow
31375
/* Les microarchitectures K7, K8 et K10 d'AMD */
745702
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons d'abord voir les architectures K5 et K6 en même temps. La raison est qu'elles ont des unités de calcul similaires, mais que le système d'exécution dans le désordre est lui totalement différent. Les architectures K7, K8 et K10 sont quant à elles similaires et seront vues en même temps. Puis, nous passerons rapidement sur les architectures Bulldozer et dérivés, pour voir les architectures Zen.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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 disposent aussi d'un cache de prédécodage, où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Ce 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. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. L'AMD K7 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
La prédiction de branchement reprend les acquis du K5/K6 : un BTB 2048 entrées, un prédicteur d'adresse de retour de 12 adresses. Mais le K7 ajoute un prédicteur dglobal, basé sur un historique. Il s'agit précisément d'un ''global history bimodal counter''.
Les architectures K7, K8 et K10 utilisent toujours une hiérarchie mémoire similaire. En premier lieu, on trouve un cache L1 d'instruction et un cache L1 de données avec une TLB par caches L1. L'introduction du cache L2 a entrainé l'ajout d'une TLB de second niveau. Ou devrais-je dire l'ajout de deux TLB de second niveau, avec une L2 TLB pour les données et une autre pour les instructions.
Le processeur conserve aussi le cache de prédécodage et un cache d'instruction pré-décodé. Il fonctionne comme sur le K5/K6, à savoir qu'il trouve les limites des instruction, trouve les octets d'opcode et détermine quel décodeur utiliser. De plus, et c'est une nouveauté, il peut reconnaitre les branchements inconditionnels directement lors du pré-décodage. De plus, 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.
Niveau décodage, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. 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''. Elles 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.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L''''architecture K7''' des processeurs Athlon décodait trois instructions par cycle et avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. La fenêtre d'instruction entière pouvait émettre trois micro-opérations : trois micro-opérations entières, trois micro-opération mémoire.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
7btc29xaolr7m4w0njvx0ynywry7f8e
745703
745702
2025-07-02T12:08:31Z
Mewtow
31375
/* Les microarchitectures K7, K8 et K10 d'AMD */
745703
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons d'abord voir les architectures K5 et K6 en même temps. La raison est qu'elles ont des unités de calcul similaires, mais que le système d'exécution dans le désordre est lui totalement différent. Les architectures K7, K8 et K10 sont quant à elles similaires et seront vues en même temps. Puis, nous passerons rapidement sur les architectures Bulldozer et dérivés, pour voir les architectures Zen.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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 disposent aussi d'un cache de prédécodage, où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Ce 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. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. L'AMD K7 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
La prédiction de branchement reprend les acquis du K5/K6 : un BTB 2048 entrées, un prédicteur d'adresse de retour de 12 adresses. Mais le K7 ajoute un prédicteur dglobal, basé sur un historique. Il s'agit précisément d'un ''global history bimodal counter''.
Les architectures K7, K8 et K10 utilisent toujours une hiérarchie mémoire similaire. En premier lieu, on trouve un cache L1 d'instruction et un cache L1 de données avec une TLB par caches L1. L'introduction du cache L2 a entrainé l'ajout d'une TLB de second niveau. Ou devrais-je dire l'ajout de deux TLB de second niveau, avec une L2 TLB pour les données et une autre pour les instructions.
Le processeur conserve aussi le cache de prédécodage et un cache d'instruction pré-décodé. Il fonctionne comme sur le K5/K6, à savoir qu'il trouve les limites des instruction, trouve les octets d'opcode et détermine quel décodeur utiliser. De plus, et c'est une nouveauté, il peut reconnaitre les branchements inconditionnels directement lors du pré-décodage. De plus, 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.
Niveau décodage, 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’à 6 instructions non-microcodées. 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. 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''.
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.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L''''architecture K7''' des processeurs Athlon décodait trois instructions par cycle et avait un tampon de ré-ordonnancement de 72 instructions, nommé ''instruction control unit'' dans le schéma qui suit. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. La fenêtre d'instruction entière pouvait émettre trois micro-opérations : trois micro-opérations entières, trois micro-opération mémoire.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
s9jr320ioispjpaxykxvqjpx5anln5w
745704
745703
2025-07-02T12:16:05Z
Mewtow
31375
/* Les microarchitectures K7, K8 et K10 d'AMD */
745704
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons d'abord voir les architectures K5 et K6 en même temps. La raison est qu'elles ont des unités de calcul similaires, mais que le système d'exécution dans le désordre est lui totalement différent. Les architectures K7, K8 et K10 sont quant à elles similaires et seront vues en même temps. Puis, nous passerons rapidement sur les architectures Bulldozer et dérivés, pour voir les architectures Zen.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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 disposent aussi d'un cache de prédécodage, où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Ce 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. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. L'AMD K7 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
La prédiction de branchement reprend les acquis du K5/K6 : un BTB 2048 entrées, un prédicteur d'adresse de retour de 12 adresses. Mais le K7 ajoute un prédicteur dglobal, basé sur un historique. Il s'agit précisément d'un ''global history bimodal counter''.
Les architectures K7, K8 et K10 utilisent toujours une hiérarchie mémoire similaire. En premier lieu, on trouve un cache L1 d'instruction et un cache L1 de données avec une TLB par caches L1. L'introduction du cache L2 a entrainé l'ajout d'une TLB de second niveau. Ou devrais-je dire l'ajout de deux TLB de second niveau, avec une L2 TLB pour les données et une autre pour les instructions.
Le processeur conserve aussi le cache de prédécodage et un cache d'instruction pré-décodé. Il fonctionne comme sur le K5/K6, à savoir qu'il trouve les limites des instruction, trouve les octets d'opcode et détermine quel décodeur utiliser. De plus, et c'est une nouveauté, il peut reconnaitre les branchements inconditionnels directement lors du pré-décodage. De plus, 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.
Niveau décodage, 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 non-microcodées. 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. 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L''''architecture K7''' des processeurs Athlon décodait trois instructions par cycle et avait un tampon de ré-ordonnancement de 72 instructions. Elle avait deux fenêtres d'instructions : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. Le processeur utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. La fenêtre d'instruction entière pouvait émettre trois micro-opérations : trois micro-opérations entières, trois micro-opération mémoire.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
a60nvoy9gzgmg9j6thwf9wck36foita
745705
745704
2025-07-02T12:22:19Z
Mewtow
31375
/* Les microarchitectures K7, K8 et K10 d'AMD */
745705
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons d'abord voir les architectures K5 et K6 en même temps. La raison est qu'elles ont des unités de calcul similaires, mais que le système d'exécution dans le désordre est lui totalement différent. Les architectures K7, K8 et K10 sont quant à elles similaires et seront vues en même temps. Puis, nous passerons rapidement sur les architectures Bulldozer et dérivés, pour voir les architectures Zen.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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 disposent aussi d'un cache de prédécodage, où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Ce 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. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. L'AMD K7 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
La prédiction de branchement reprend les acquis du K5/K6 : un BTB 2048 entrées, un prédicteur d'adresse de retour de 12 adresses. Mais le K7 ajoute un prédicteur dglobal, basé sur un historique. Il s'agit précisément d'un ''global history bimodal counter''.
Les architectures K7, K8 et K10 utilisent toujours une hiérarchie mémoire similaire. En premier lieu, on trouve un cache L1 d'instruction et un cache L1 de données avec une TLB par caches L1. L'introduction du cache L2 a entrainé l'ajout d'une TLB de second niveau. Ou devrais-je dire l'ajout de deux TLB de second niveau, avec une L2 TLB pour les données et une autre pour les instructions.
Le processeur conserve aussi le cache de prédécodage et un cache d'instruction pré-décodé. Il fonctionne comme sur le K5/K6, à savoir qu'il trouve les limites des instruction, trouve les octets d'opcode et détermine quel décodeur utiliser. De plus, et c'est une nouveauté, il peut reconnaitre les branchements inconditionnels directement lors du pré-décodage. De plus, 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.
Niveau décodage, 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. 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L''''architecture K7''' des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les '''architectures K8 et K10''' sont assez similaires. La différence principale est la présence d'un cache L3, d'une unité de calcul supplémentaire et de l'ajout d'une unité de prédiction des branchements indirects. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
gjh2nxyacv33j6h0v5mdwb96b0eipyt
745706
745705
2025-07-02T12:26:37Z
Mewtow
31375
/* Les microarchitectures K7, K8 et K10 d'AMD */
745706
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons d'abord voir les architectures K5 et K6 en même temps. La raison est qu'elles ont des unités de calcul similaires, mais que le système d'exécution dans le désordre est lui totalement différent. Les architectures K7, K8 et K10 sont quant à elles similaires et seront vues en même temps. Puis, nous passerons rapidement sur les architectures Bulldozer et dérivés, pour voir les architectures Zen.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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 disposent aussi d'un cache de prédécodage, où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Ce 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. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les architectures K8 et K10 sont assez similaires à l'architecture K7. La différence principale est 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.
La prédiction de branchement reprend les acquis du K5/K6 : un BTB 2048 entrées, un prédicteur d'adresse de retour de 12 adresses. Mais le K7 ajoute un prédicteur global, basé sur un historique. Il s'agit précisément d'un ''global history bimodal counter''. Les architectures K8 et K10 ajoutent une unité de prédiction des branchements indirects.
Les architectures K7, K8 et K10 utilisent toujours une hiérarchie mémoire similaire. En premier lieu, on trouve un cache L1 d'instruction et un cache L1 de données avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. L'introduction du cache L2 a entrainé l'ajout d'une TLB de second niveau. Ou devrais-je dire l'ajout de deux TLB de second niveau, avec une L2 TLB pour les données et une autre pour les instructions.
: L'AMD K7 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Le processeur conserve aussi le cache de prédécodage et un cache d'instruction pré-décodé. Il fonctionne comme sur le K5/K6, à savoir qu'il trouve les limites des instruction, trouve les octets d'opcode et détermine quel décodeur utiliser. De plus, et c'est une nouveauté, il peut reconnaitre les branchements inconditionnels directement lors du pré-décodage. De plus, 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.
Niveau décodage, 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. 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
hk4o8jk4fd3u5wvr538634dfgspwlv4
745707
745706
2025-07-02T12:29:12Z
Mewtow
31375
/* Les microarchitectures K7, K8 et K10 d'AMD */
745707
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons d'abord voir les architectures K5 et K6 en même temps. La raison est qu'elles ont des unités de calcul similaires, mais que le système d'exécution dans le désordre est lui totalement différent. Les architectures K7, K8 et K10 sont quant à elles similaires et seront vues en même temps. Puis, nous passerons rapidement sur les architectures Bulldozer et dérivés, pour voir les architectures Zen.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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 disposent aussi d'un cache de prédécodage, où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Ce 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. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les architectures K8 et K10 sont assez similaires à l'architecture K7. La différence principale est 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.
La prédiction de branchement reprend les acquis du K5/K6 : un BTB 2048 entrées, un prédicteur d'adresse de retour de 12 adresses. Mais le K7 ajoute un prédicteur global, basé sur un historique. Il s'agit précisément d'un ''global history bimodal counter''. Les architectures K8 et K10 ajoutent une unité de prédiction des branchements indirects.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Le processeur conserve aussi le cache de prédécodage et un cache d'instruction pré-décodé. Il fonctionne comme sur le K5/K6, à savoir qu'il trouve les limites des instruction, trouve les octets d'opcode et détermine quel décodeur utiliser. De plus, et c'est une nouveauté, il peut reconnaitre les branchements inconditionnels directement lors du pré-décodage. De plus, 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.
Niveau décodage, 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. 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
pmfg5enmomo28yqs3gc725xtkjwphka
745708
745707
2025-07-02T12:38:39Z
Mewtow
31375
/* Les microarchitectures K7, K8 et K10 d'AMD */
745708
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons d'abord voir les architectures K5 et K6 en même temps. La raison est qu'elles ont des unités de calcul similaires, mais que le système d'exécution dans le désordre est lui totalement différent. Les architectures K7, K8 et K10 sont quant à elles similaires et seront vues en même temps. Puis, nous passerons rapidement sur les architectures Bulldozer et dérivés, pour voir les architectures Zen.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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 disposent aussi d'un cache de prédécodage, où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Ce 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. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les architectures K8 et K10 sont assez similaires à l'architecture K7. La différence principale est 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.
La prédiction de branchement reprend les acquis du K5/K6 : un BTB 2048 entrées, un prédicteur d'adresse de retour de 12 adresses. Mais le K7 ajoute un prédicteur global, basé sur un historique. Il s'agit précisément d'un ''global history bimodal counter''. Les architectures K8 et K10 ajoutent une unité de prédiction des branchements indirects.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Le processeur conserve aussi le cache de prédécodage et un cache d'instruction pré-décodé. Il fonctionne comme sur le K5/K6, à savoir qu'il trouve les limites des instruction, trouve les octets d'opcode et détermine quel décodeur utiliser. De plus, et c'est une nouveauté, il peut reconnaitre les branchements inconditionnels directement lors du pré-décodage. De plus, 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.
Niveau décodage, 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. 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
mxk8fyik155cpj9llwb3ucg0j93duko
745709
745708
2025-07-02T12:41:10Z
Mewtow
31375
/* Les microarchitectures K7, K8 et K10 d'AMD */
745709
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons d'abord voir les architectures K5 et K6 en même temps. La raison est qu'elles ont des unités de calcul similaires, mais que le système d'exécution dans le désordre est lui totalement différent. Les architectures K7, K8 et K10 sont quant à elles similaires et seront vues en même temps. Puis, nous passerons rapidement sur les architectures Bulldozer et dérivés, pour voir les architectures Zen.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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 disposent aussi d'un cache de prédécodage, où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Ce 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. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les architectures K8 et K10 sont assez similaires à l'architecture K7. La différence principale est 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.
La prédiction de branchement reprend les acquis du K5/K6 : un BTB 2048 entrées, un prédicteur d'adresse de retour de 12 adresses. Mais le K7 ajoute un prédicteur global, basé sur un historique. Il s'agit précisément d'un ''global history bimodal counter''. Les architectures K8 et K10 ajoutent une unité de prédiction des branchements indirects.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Le processeur conserve aussi le cache de prédécodage et un cache d'instruction pré-décodé. Il fonctionne comme sur le K5/K6, à savoir qu'il trouve les limites des instruction, trouve les octets d'opcode et détermine quel décodeur utiliser. De plus, et c'est une nouveauté, il peut reconnaitre les branchements inconditionnels directement lors du pré-décodage. De plus, 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.
Niveau décodage, 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. 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
datdlxxw9ou19rxvl1dm02fho7dzvo1
745710
745709
2025-07-02T12:44:31Z
Mewtow
31375
/* Les microarchitectures K7, K8 et K10 d'AMD */
745710
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons d'abord voir les architectures K5 et K6 en même temps. La raison est qu'elles ont des unités de calcul similaires, mais que le système d'exécution dans le désordre est lui totalement différent. Les architectures K7, K8 et K10 sont quant à elles similaires et seront vues en même temps. Puis, nous passerons rapidement sur les architectures Bulldozer et dérivés, pour voir les architectures Zen.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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 disposent aussi d'un cache de prédécodage, où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Ce 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. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. 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.
est 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.
La prédiction de branchement reprend les acquis du K5/K6 : un BTB 2048 entrées, un prédicteur d'adresse de retour de 12 adresses. Mais le K7 ajoute un prédicteur global, basé sur un historique. Il s'agit précisément d'un ''global history bimodal counter''. Les architectures K8 et K10 ajoutent une unité de prédiction des branchements indirects.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Le processeur conserve aussi le cache de prédécodage et un cache d'instruction pré-décodé. Il fonctionne comme sur le K5/K6, à savoir qu'il trouve les limites des instruction, trouve les octets d'opcode et détermine quel décodeur utiliser. De plus, et c'est une nouveauté, il peut reconnaitre les branchements inconditionnels directement lors du pré-décodage. De plus, 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.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
gob7j7ciqt9bfoi1zu4hpjkn3cv4680
745711
745710
2025-07-02T12:47:32Z
Mewtow
31375
/* Les microarchitectures K7, K8 et K10 d'AMD */
745711
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons d'abord voir les architectures K5 et K6 en même temps. La raison est qu'elles ont des unités de calcul similaires, mais que le système d'exécution dans le désordre est lui totalement différent. Les architectures K7, K8 et K10 sont quant à elles similaires et seront vues en même temps. Puis, nous passerons rapidement sur les architectures Bulldozer et dérivés, pour voir les architectures Zen.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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 disposent aussi d'un cache de prédécodage, où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Ce 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. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. 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.
est 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.
La prédiction de branchement reprend les acquis du K5/K6 : un BTB de 2048 entrées, un prédicteur d'adresse de retour de 12 adresses. Mais elle ajoute une unité de prédiction des branchements indirects et un prédicteur global, basé sur un historique. Précisément, c'est un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Le processeur conserve aussi le cache de prédécodage et un cache d'instruction pré-décodé. Il fonctionne comme sur le K5/K6, à savoir qu'il trouve les limites des instruction, trouve les octets d'opcode et détermine quel décodeur utiliser. De plus, et c'est une nouveauté, il peut reconnaitre les branchements inconditionnels directement lors du pré-décodage. De plus, 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.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
nxf9cges4zcb6ohj99hpaw48dbxehjf
745712
745711
2025-07-02T12:50:37Z
Mewtow
31375
/* Les microarchitectures K5 et K6 d'AMD */
745712
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons d'abord voir les architectures K5 et K6 en même temps. La raison est qu'elles ont des unités de calcul similaires, mais que le système d'exécution dans le désordre est lui totalement différent. Les architectures K7, K8 et K10 sont quant à elles similaires et seront vues en même temps. Puis, nous passerons rapidement sur les architectures Bulldozer et dérivés, pour voir les architectures Zen.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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 disposent aussi d'un cache de prédécodage, où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Ce 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. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. 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.
est 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.
La prédiction de branchement reprend les acquis du K5/K6 : un BTB de 2048 entrées, un prédicteur d'adresse de retour de 12 adresses. Mais elle ajoute une unité de prédiction des branchements indirects et un prédicteur global, basé sur un historique. Précisément, c'est un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Le processeur conserve aussi le cache de prédécodage et un cache d'instruction pré-décodé. Il fonctionne comme sur le K5/K6, à savoir qu'il trouve les limites des instruction, trouve les octets d'opcode et détermine quel décodeur utiliser. De plus, et c'est une nouveauté, il peut reconnaitre les branchements inconditionnels directement lors du pré-décodage. De plus, 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.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
rgvl6i1mox3qkqolrletapo3ruhcfi3
745713
745712
2025-07-02T12:54:29Z
Mewtow
31375
/* Les microarchitectures K7, K8 et K10 d'AMD */
745713
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons d'abord voir les architectures K5 et K6 en même temps. La raison est qu'elles ont des unités de calcul similaires, mais que le système d'exécution dans le désordre est lui totalement différent. Les architectures K7, K8 et K10 sont quant à elles similaires et seront vues en même temps. Puis, nous passerons rapidement sur les architectures Bulldozer et dérivés, pour voir les architectures Zen.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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 disposent aussi d'un cache de prédécodage, où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Ce 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. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. 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.
est 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.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Le processeur conserve aussi le cache de prédécodage et un cache d'instruction pré-décodé. Il fonctionne comme sur le K5/K6, à savoir qu'il trouve les limites des instruction, trouve les octets d'opcode et détermine quel décodeur utiliser. De plus, et c'est une nouveauté, il peut reconnaitre les branchements inconditionnels directement lors du pré-décodage. De plus, 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.
La prédiction de branchement reprend les acquis du K5/K6 : un BTB de 2048 entrées, un prédicteur d'adresse de retour de 12 adresses. Mais elle ajoute une unité de prédiction des branchements indirects et un prédicteur global, basé sur un historique. Précisément, c'est un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8. Il faut noter que la prédiction de branchement est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner. Au passage, ce fonctionnement colle bien avec le fait que le L2 soit un cache de victime.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
sofa3ij2dsdr2emzlkhed31jp0x3acg
745716
745713
2025-07-02T12:58:53Z
Mewtow
31375
/* Les microarchitectures K7, K8 et K10 d'AMD */
745716
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons d'abord voir les architectures K5 et K6 en même temps. La raison est qu'elles ont des unités de calcul similaires, mais que le système d'exécution dans le désordre est lui totalement différent. Les architectures K7, K8 et K10 sont quant à elles similaires et seront vues en même temps. Puis, nous passerons rapidement sur les architectures Bulldozer et dérivés, pour voir les architectures Zen.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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 disposent aussi d'un cache de prédécodage, où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Ce 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. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. 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.
est 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.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Le processeur conserve aussi le cache de prédécodage et un cache d'instruction pré-décodé. Il fonctionne comme sur le K5/K6, à savoir qu'il trouve les limites des instruction, trouve les octets d'opcode et détermine quel décodeur utiliser. De plus, et c'est une nouveauté, il peut reconnaitre les branchements inconditionnels directement lors du pré-décodage. De plus, 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.
La prédiction de branchement reprend les acquis du K5/K6 : un BTB de 2048 entrées, un prédicteur d'adresse de retour de 12 adresses. Mais elle ajoute une unité de prédiction des branchements indirects et un prédicteur global, basé sur un historique. Précisément, c'est un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8. Il faut noter que la prédiction de branchement est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner. Au passage, ce fonctionnement colle bien avec le fait que le L2 soit un cache de victime.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
tv7kdqsc4sw1kcx2f0tf3ljjo6suc28
745718
745716
2025-07-02T13:08:47Z
Mewtow
31375
/* Les microarchitectures K7, K8 et K10 d'AMD */
745718
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons d'abord voir les architectures K5 et K6 en même temps. La raison est qu'elles ont des unités de calcul similaires, mais que le système d'exécution dans le désordre est lui totalement différent. Les architectures K7, K8 et K10 sont quant à elles similaires et seront vues en même temps. Puis, nous passerons rapidement sur les architectures Bulldozer et dérivés, pour voir les architectures Zen.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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 disposent aussi d'un cache de prédécodage, où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Ce 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. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. 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.
est 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.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Le processeur conserve aussi le cache de prédécodage et un cache d'instruction pré-décodé. Il fonctionne comme sur le K5/K6, à savoir qu'il trouve les limites des instruction, trouve les octets d'opcode et détermine quel décodeur utiliser. De plus, et c'est une nouveauté, il peut reconnaitre les branchements inconditionnels directement lors du pré-décodage. De plus, 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.
La prédiction de branchement reprend les acquis du K5/K6 : un BTB de 2048 entrées, un prédicteur d'adresse de retour de 12 adresses. Mais elle ajoute une unité de prédiction des branchements indirects et un prédicteur global, basé sur un historique. Précisément, c'est un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8. Il faut noter que la prédiction de branchement est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner. Au passage, ce fonctionnement colle bien avec le fait que le L2 soit un cache de victime.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. 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, là où les pipelines entiers sont alimentés par un système d'exécution dans le désordre moins complexe mais suffisant pour sa tâche.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
etvjnd5ohk8e1m0meilerijo21kyzes
745719
745718
2025-07-02T13:11:15Z
Mewtow
31375
/* Les microarchitectures K7, K8 et K10 d'AMD */
745719
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons d'abord voir les architectures K5 et K6 en même temps. La raison est qu'elles ont des unités de calcul similaires, mais que le système d'exécution dans le désordre est lui totalement différent. Les architectures K7, K8 et K10 sont quant à elles similaires et seront vues en même temps. Puis, nous passerons rapidement sur les architectures Bulldozer et dérivés, pour voir les architectures Zen.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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 disposent aussi d'un cache de prédécodage, où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Ce 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. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. 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.
est 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.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Le processeur conserve aussi le cache de prédécodage et un cache d'instruction pré-décodé. Il fonctionne comme sur le K5/K6, à savoir qu'il trouve les limites des instruction, trouve les octets d'opcode et détermine quel décodeur utiliser. De plus, et c'est une nouveauté, il peut reconnaitre les branchements inconditionnels directement lors du pré-décodage. De plus, 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.
La prédiction de branchement reprend les acquis du K5/K6 : un BTB de 2048 entrées, un prédicteur d'adresse de retour de 12 adresses. Mais elle ajoute une unité de prédiction des branchements indirects et un prédicteur global, basé sur un historique. Précisément, c'est un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8. Il faut noter que la prédiction de branchement est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner. Au passage, ce fonctionnement colle bien avec le fait que le L2 soit un cache de victime.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués. 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 (18 cycles au total, chargement et décodage inclus). A l'opposé, les pipelines entiers sont alimentés par un système d'exécution dans le désordre moins complexe mais suffisant pour sa tâche et qui est de seulement 15 cycles au total (chargement et décodage inclus).
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
gp8kh783bft1aa8huuzzi25mvyaphts
745720
745719
2025-07-02T13:14:02Z
Mewtow
31375
/* Les microarchitectures K7, K8 et K10 d'AMD */
745720
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons d'abord voir les architectures K5 et K6 en même temps. La raison est qu'elles ont des unités de calcul similaires, mais que le système d'exécution dans le désordre est lui totalement différent. Les architectures K7, K8 et K10 sont quant à elles similaires et seront vues en même temps. Puis, nous passerons rapidement sur les architectures Bulldozer et dérivés, pour voir les architectures Zen.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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 disposent aussi d'un cache de prédécodage, où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Ce 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. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. 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.
est 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.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Le processeur conserve aussi le cache de prédécodage et un cache d'instruction pré-décodé. Il fonctionne comme sur le K5/K6, à savoir qu'il trouve les limites des instruction, trouve les octets d'opcode et détermine quel décodeur utiliser. De plus, et c'est une nouveauté, il peut reconnaitre les branchements inconditionnels directement lors du pré-décodage. De plus, 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.
La prédiction de branchement reprend les acquis du K5/K6 : un BTB de 2048 entrées, un prédicteur d'adresse de retour de 12 adresses. Mais elle ajoute une unité de prédiction des branchements indirects et un prédicteur global, basé sur un historique. Précisément, c'est un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8. Il faut noter que la prédiction de branchement est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner. Au passage, ce fonctionnement colle bien avec le fait que le L2 soit un cache de victime.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
pktb3zvw82i3djjj9h4oov20zc0aik7
745721
745720
2025-07-02T13:19:32Z
Mewtow
31375
/* Un étude des microarchitectures superscalaires x86 d'AMD */
745721
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuités, où AMD a revu sa copie de fond en comble.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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 disposent aussi d'un cache de prédécodage, où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Ce 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. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. 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.
est 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.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Le processeur conserve aussi le cache de prédécodage et un cache d'instruction pré-décodé. Il fonctionne comme sur le K5/K6, à savoir qu'il trouve les limites des instruction, trouve les octets d'opcode et détermine quel décodeur utiliser. De plus, et c'est une nouveauté, il peut reconnaitre les branchements inconditionnels directement lors du pré-décodage. De plus, 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.
La prédiction de branchement reprend les acquis du K5/K6 : un BTB de 2048 entrées, un prédicteur d'adresse de retour de 12 adresses. Mais elle ajoute une unité de prédiction des branchements indirects et un prédicteur global, basé sur un historique. Précisément, c'est un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8. Il faut noter que la prédiction de branchement est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner. Au passage, ce fonctionnement colle bien avec le fait que le L2 soit un cache de victime.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
re9rzsn80rsihuw22dvq4cznd4s9r24
745722
745721
2025-07-02T13:26:44Z
Mewtow
31375
/* Un étude des microarchitectures superscalaires x86 d'AMD */
745722
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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 disposent aussi d'un cache de prédécodage, où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Ce 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. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. 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.
est 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.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Le processeur conserve aussi le cache de prédécodage et un cache d'instruction pré-décodé. Il fonctionne comme sur le K5/K6, à savoir qu'il trouve les limites des instruction, trouve les octets d'opcode et détermine quel décodeur utiliser. De plus, et c'est une nouveauté, il peut reconnaitre les branchements inconditionnels directement lors du pré-décodage. De plus, 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.
La prédiction de branchement reprend les acquis du K5/K6 : un BTB de 2048 entrées, un prédicteur d'adresse de retour de 12 adresses. Mais elle ajoute une unité de prédiction des branchements indirects et un prédicteur global, basé sur un historique. Précisément, c'est un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8. Il faut noter que la prédiction de branchement est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner. Au passage, ce fonctionnement colle bien avec le fait que le L2 soit un cache de victime.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
cw6o0f5bcqtvfr844mqw9afc7thkl09
745723
745722
2025-07-02T13:33:59Z
Mewtow
31375
/* Un étude des microarchitectures superscalaires x86 d'AMD */
745723
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de processeurs AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10.
Elles disposent aussi d'un cache de 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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits.
est 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.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
La prédiction de branchement reprend les acquis du K5/K6 : un BTB de 2048 entrées, un prédicteur d'adresse de retour de 12 adresses. Mais elle ajoute une unité de prédiction des branchements indirects et un prédicteur global, basé sur un historique. Précisément, c'est un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8. Il faut noter que la prédiction de branchement est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner. Au passage, ce fonctionnement colle bien avec le fait que le L2 soit un cache de victime.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
jhlzunoeyd3amlt3ludxp7bm5x2it99
745724
745723
2025-07-02T13:36:19Z
Mewtow
31375
/* Un étude des microarchitectures superscalaires x86 d'AMD */
745724
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10.
Elles disposent aussi d'un système de '''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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses. L'architecture K7 ajoute une unité de prédiction des branchements indirects et un prédicteur global, basé sur un historique. Précisément, c'est un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8.
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner. Au passage, ce fonctionnement colle bien avec le fait que le L2 soit un cache de victime.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
ov3v32tdye40wp42gc017c9mk83v4nk
745725
745724
2025-07-02T13:37:41Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745725
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10.
Elles disposent aussi d'un système de '''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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses. L'architecture K7 ajoute une unité de prédiction des branchements indirects et un prédicteur global, basé sur un historique. Précisément, c'est un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8.
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
qjwojn0fhewcez2n4isxav6phruagts
745726
745725
2025-07-02T13:38:03Z
Mewtow
31375
/* Les microarchitectures K5 et K6 d'AMD */
745726
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10.
Elles disposent aussi d'un système de '''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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses. L'architecture K7 ajoute une unité de prédiction des branchements indirects et un prédicteur global, basé sur un historique. Précisément, c'est un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8.
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
had19gkfuqn03vfnbcwlkqcy2p2mgqq
745727
745726
2025-07-02T13:38:11Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745727
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10.
Elles disposent aussi d'un système de '''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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, avec un compteur à saturation de 1 bit pour chaque branchement. Le tout a été améliorée lors du passage au K6. Les compteurs de saturation passent à deux bits, et le BTB passe à 8192 branchements. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses. L'architecture K7 ajoute une unité de prédiction des branchements indirects et un prédicteur global, basé sur un historique. Précisément, c'est un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8.
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
rgqwdlksqgowv3crv3jsmiln444bkn5
745728
745727
2025-07-02T13:54:46Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745728
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10.
Elles disposent aussi d'un système de '''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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, des compteurs à saturation de 1 bit. Sur le K6, le BTB est remplacé par une ''Branch History Table'' de 8192 entrées avec des compteurs à saturation de 2 bits. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses. L'architecture K7 ajoute une unité de prédiction des branchements indirects et remplace la ''Branch History Table'' par un prédicteur hybride, qui combine un BTB avec un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
mhczt1a4zasqrl2o1uwmcqbzm8w5mk1
745729
745728
2025-07-02T13:56:11Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745729
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB par cache : une pour le L1 d'instruction, une pour le L1 de données. Pour le cache L2, il y avait deux TLB : une pour les données, une pour les instructions.
Elles disposent aussi d'un système de '''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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, des compteurs à saturation de 1 bit. Sur le K6, le BTB est remplacé par une ''Branch History Table'' de 8192 entrées avec des compteurs à saturation de 2 bits. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses. L'architecture K7 ajoute une unité de prédiction des branchements indirects et remplace la ''Branch History Table'' par un prédicteur hybride, qui combine un BTB avec un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
81z9gagtw44qjiszyzskss0burkpkf8
745730
745729
2025-07-02T13:58:39Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745730
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB par cache : une pour le L1 d'instruction, une pour le L1 de données. Pour le cache L2, il y avait deux TLB : une pour les données, une pour les instructions.
{[class="wikitable"
|-
! Architecture AMD
! colspan="2" | Caches
! colspan="2" | TLBs
|-
| K5
| L1 instruction || L1 données
| colspan="2" | TLB unique
|-
| K6
| L1 instruction || L1 données
| TLB L1 instruction || TLB L1 données
|-
| K7
|
|
|-
| K8
|
|
|-
| K10
|
|
|}
Elles disposent aussi d'un système de '''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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, des compteurs à saturation de 1 bit. Sur le K6, le BTB est remplacé par une ''Branch History Table'' de 8192 entrées avec des compteurs à saturation de 2 bits. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses. L'architecture K7 ajoute une unité de prédiction des branchements indirects et remplace la ''Branch History Table'' par un prédicteur hybride, qui combine un BTB avec un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
296ycchp4tth3uqo6zqkrzra4b3echy
745731
745730
2025-07-02T14:01:09Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745731
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB par cache : une pour le L1 d'instruction, une pour le L1 de données. Pour le cache L2, il y avait deux TLB : une pour les données, une pour les instructions.
{|class="wikitable"
|-
! Architecture AMD
! colspan="2" | Caches
! colspan="2" | TLBs
|-
| K5
| L1 instruction || L1 données
| colspan="2" | TLB unique
|-
| colspan="4" |
|-
| K6
| L1 instruction || L1 données
| TLB L1 instruction || TLB L1 données
|-
| colspan="4" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données
| TLB L1 instruction || TLB L1 données
|-
| colspan="2" | L2 unifié
| TLB L2 instruction || TLB L2 données
|-
| colspan="4" |
|-
| rowspan="3" | K10
| L1 instruction || L1 données
| TLB L1 instruction || TLB L1 données
|-
| colspan="2" | L2 unifié
| TLB L2 instruction || TLB L2 données
|-
| colspan="2" | L3 unifié
| colspan="2" |
|}
Elles disposent aussi d'un système de '''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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, des compteurs à saturation de 1 bit. Sur le K6, le BTB est remplacé par une ''Branch History Table'' de 8192 entrées avec des compteurs à saturation de 2 bits. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses. L'architecture K7 ajoute une unité de prédiction des branchements indirects et remplace la ''Branch History Table'' par un prédicteur hybride, qui combine un BTB avec un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
jncl539jdfnra3fzo4c1b4f7zohcagq
745732
745731
2025-07-02T14:03:23Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745732
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB par cache : une pour le L1 d'instruction, une pour le L1 de données. Pour le cache L2, il y avait deux TLB : une pour les données, une pour les instructions.
{|class="wikitable"
|-
! Architecture AMD
! colspan="2" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données
|-
| TLB unique
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || L2 unifié
|-
| TLB L1 instruction || TLB L1 données
|-
| colspan="4" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données
|-
| colspan="4" |
|-
| rowspan="3" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
Elles disposent aussi d'un système de '''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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, des compteurs à saturation de 1 bit. Sur le K6, le BTB est remplacé par une ''Branch History Table'' de 8192 entrées avec des compteurs à saturation de 2 bits. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses. L'architecture K7 ajoute une unité de prédiction des branchements indirects et remplace la ''Branch History Table'' par un prédicteur hybride, qui combine un BTB avec un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
kpxlzsc71y9lp1f4klmbx0j900dfhjp
745733
745732
2025-07-02T14:05:14Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745733
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB par cache : une pour le L1 d'instruction, une pour le L1 de données. Pour le cache L2, il y avait deux TLB : une pour les données, une pour les instructions.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="3" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
Elles disposent aussi d'un système de '''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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, des compteurs à saturation de 1 bit. Sur le K6, le BTB est remplacé par une ''Branch History Table'' de 8192 entrées avec des compteurs à saturation de 2 bits. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses. L'architecture K7 ajoute une unité de prédiction des branchements indirects et remplace la ''Branch History Table'' par un prédicteur hybride, qui combine un BTB avec un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
f8xegav9z3q35b2906l0hpszz6guts1
745734
745733
2025-07-02T14:05:25Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745734
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB par cache : une pour le L1 d'instruction, une pour le L1 de données. Pour le cache L2, il y avait deux TLB : une pour les données, une pour les instructions.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
Elles disposent aussi d'un système de '''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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, des compteurs à saturation de 1 bit. Sur le K6, le BTB est remplacé par une ''Branch History Table'' de 8192 entrées avec des compteurs à saturation de 2 bits. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses. L'architecture K7 ajoute une unité de prédiction des branchements indirects et remplace la ''Branch History Table'' par un prédicteur hybride, qui combine un BTB avec un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
mdljwovkj0azifl0v4a0fsnegnqqw3m
745735
745734
2025-07-02T14:06:15Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745735
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
Elles disposent aussi d'un système de '''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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, des compteurs à saturation de 1 bit. Sur le K6, le BTB est remplacé par une ''Branch History Table'' de 8192 entrées avec des compteurs à saturation de 2 bits. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses. L'architecture K7 ajoute une unité de prédiction des branchements indirects et remplace la ''Branch History Table'' par un prédicteur hybride, qui combine un BTB avec un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8.
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
leg8toqpnw3qj3853kv5orn7te38bbt
745736
745735
2025-07-02T14:06:35Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745736
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
La prédiction de branchement du K5 était très simple : un BTB de 1024 branchements, des compteurs à saturation de 1 bit. Sur le K6, le BTB est remplacé par une ''Branch History Table'' de 8192 entrées avec des compteurs à saturation de 2 bits. Le K6 intègre aussi un prédicteur de retour de fonction, avec une pile d'adresse de 16 adresses. L'architecture K7 ajoute une unité de prédiction des branchements indirects et remplace la ''Branch History Table'' par un prédicteur hybride, qui combine un BTB avec un ''global history bimodal counter'', allant de 4096 entrées sur le K7 à 16384 sur le K8.
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.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
Elles disposent aussi d'un système de '''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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
93ncsefxm7wcawkav64mnl3bcyven5c
745737
745736
2025-07-02T14:06:44Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745737
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
Elles disposent aussi d'un système de '''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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
ks55zbs78i6sim0yu55pp1d81y8a88f
745738
745737
2025-07-02T14:08:45Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745738
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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 est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Les architectures K7, K8 et K10 disposent d'un cache L1 d'instruction et d'un cache L1 de données, avec une TLB par caches L1. Un changement notable comparé aux architectures précédentes est l'introduction du cache L2, en plus des deux caches L1. 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 originelle, 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'Ahtlon XP, ont intégré le cache L2 dans le processeur.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
p5betxr4n00h1jvp27fygixd3zomsbp
745739
745738
2025-07-02T14:09:11Z
Mewtow
31375
/* Les microarchitectures K7, K8 et K10 d'AMD */
745739
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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 est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
cmymhcmjdil0s8tp97eqhk7gf6i5e0t
745740
745739
2025-07-02T14:09:21Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745740
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Ahtlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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 est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
84k5gvaxbw0h7yzasge6py8tel2nmk9
745741
745740
2025-07-02T14:09:32Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745741
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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 est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
93oo0c6bttjm6m81x2zf9pt942z0r5i
745742
745741
2025-07-02T14:10:00Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745742
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient des caches L1, mais pas de cache L2. Elles avaient chacune un cache L1 d'instruction, et un cache L1 de données. Les deux étaient reliés à une interface avec le ''northbridge''. 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. Le chargement des instructions copie les instructions du cache d'instruction dans une file d'instruction de 16 octets.
: 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''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
lr5xxzabe0x66xvjdhlg364qga0z1zf
745743
745742
2025-07-02T14:10:56Z
Mewtow
31375
/* Les microarchitectures K5 et K6 d'AMD */
745743
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
===Les microarchitectures K5 et K6 d'AMD===
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
hcuei4oc4gt85k7lsd0xp7s1znapj9s
745744
745743
2025-07-02T14:11:19Z
Mewtow
31375
/* Les microarchitectures K5 et K6 d'AMD */
745744
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
====Les microarchitectures K5 et K6 d'AMD====
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
===Les microarchitectures K7, K8 et K10 d'AMD===
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
5f8vu4l0hcsnero69u26u8rh29rjh5k
745745
745744
2025-07-02T14:11:25Z
Mewtow
31375
/* Les microarchitectures K7, K8 et K10 d'AMD */
745745
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
====Les microarchitectures K5 et K6 d'AMD====
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
====Les microarchitectures K7, K8 et K10 d'AMD====
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
Niveau décodage, 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''.
Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
bt24wnmxi5gpd2c51s0jjzapycuuth2
745746
745745
2025-07-02T14:17:34Z
Mewtow
31375
/* Les microarchitectures K7, K8 et K10 d'AMD */
745746
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
====Les microarchitectures K5 et K6 d'AMD====
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''.
Au niveau du décodage, on trouve de nombreuses différences. 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 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
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 est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
====Les microarchitectures K7, K8 et K10 d'AMD====
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
bgv6hgsu0e56a2e3ymmxvv37pm3ume7
745747
745746
2025-07-02T14:18:14Z
Mewtow
31375
/* Les microarchitectures K5 et K6 d'AMD */
745747
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable.
L'AMD K6 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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''.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=2|Décodage sur le K5 et le K5]]
====Les microarchitectures K5 et K6 d'AMD====
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''.
Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc.
Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
====Les microarchitectures K7, K8 et K10 d'AMD====
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
8e8oqwqlpvcfanl59jjigh55qb5h8pu
745748
745747
2025-07-02T14:18:28Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745748
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable.
L'AMD K6 fait autrement, en utilisant moins de décodeurs. Il a lui aussi un décodeur hybride, lui-même composé de 4 sous-décodeurs : deux décodeurs simples, un décodeur pour les instructions complexes, et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, mais pas plus.
L'architecture K6 pouvait décoder 2 instructions par cycles, avec une limite de maximum 4 micro-opérations par cycle en sortie des décodeurs. Les décodeurs sont capables de décoder une ou deux instructions, en 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''.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]]
====Les microarchitectures K5 et K6 d'AMD====
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''.
Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc.
Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
====Les microarchitectures K7, K8 et K10 d'AMD====
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
kubj1316lv6rebwos1mcdr3fk5tlfjx
745749
745748
2025-07-02T14:21:06Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745749
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable.
L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs.
Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]]
====Les microarchitectures K5 et K6 d'AMD====
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''.
Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc.
Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
====Les microarchitectures K7, K8 et K10 d'AMD====
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
gtj5d66ar5lhvlgumtzj3xdywacyc90
745750
745749
2025-07-02T14:23:46Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745750
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est qu'en cas de défaut dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite, ce qui fait que le préchargement ne peut pas fonctionner.
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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable.
L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs.
Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''.
La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]]
====Les microarchitectures K5 et K6 d'AMD====
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''.
Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc.
Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
====Les microarchitectures K7, K8 et K10 d'AMD====
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
1706s4c651mxd9kcizt6ajtrjwacoi3
745758
745750
2025-07-02T15:29:10Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745758
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable.
L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs.
Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''.
La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]]
====Les microarchitectures K5 et K6 d'AMD====
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''.
Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc.
Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
====Les microarchitectures K7, K8 et K10 d'AMD====
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
ex42nxwtoj2oqhnax2jkg8l5fggz426
745760
745758
2025-07-02T15:53:30Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745760
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. 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.
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. Le décodage est ainsi fait lors du chargement de l'instruction dans le cache, et pas à chaque décodage. La différence se manifeste avec les boucles, où une instruction est exécutée plusieurs fois : le travail fait lors du prédécodage est fait une seule fois, et pas à chaque décodage. 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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable.
L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs.
Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''.
La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]]
====Les microarchitectures K5 et K6 d'AMD====
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''.
Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc.
Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
====Les microarchitectures K7, K8 et K10 d'AMD====
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
srnmxv3eylv7s5oee6wmi4kzd508fal
745797
745760
2025-07-02T18:58:24Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745797
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
La prédiction de branchement de ces CPU est fortement liée au cache L1, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. 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.
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.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable.
L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs.
Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''.
La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]]
====Les microarchitectures K5 et K6 d'AMD====
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''.
Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc.
Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
====Les microarchitectures K7, K8 et K10 d'AMD====
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
nq57i2n3cfskf7ukmhpwruc9ovzvq1f
745798
745797
2025-07-02T19:03:23Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745798
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction.
Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10.
La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable.
L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs.
Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''.
La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]]
====Les microarchitectures K5 et K6 d'AMD====
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''.
Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc.
Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
====Les microarchitectures K7, K8 et K10 d'AMD====
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
9kcy5dqfho7ejvaz6omuz5miuw1b43s
745799
745798
2025-07-02T19:04:32Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745799
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction.
Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10.
La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 mémorise l'adresse de destination du premier branchement pris, en plus des bits de pré-décodage. Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable.
L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs.
Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''.
La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]]
====Les microarchitectures K5 et K6 d'AMD====
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''.
Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc.
Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
====Les microarchitectures K7, K8 et K10 d'AMD====
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
lbvpgtlvekumjdxppnprnbja1tqfnc9
745800
745799
2025-07-02T19:06:12Z
Mewtow
31375
/* La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10 */
745800
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction.
Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10.
La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée.
Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable.
L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs.
Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''.
La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]]
====Les microarchitectures K5 et K6 d'AMD====
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''.
Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc.
Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant est capable de multiplier deux flottants de 82 bits, ce qui fait qu'il intègre un multiplieur entier pour multiplier les mantisses suffisant pour multiplier deux entiers 32 bits. 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, qui ont leur propre ALU simple simple. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
====Les microarchitectures K7, K8 et K10 d'AMD====
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
20lsu07zfr8q9bpkoc6yc9lvnz5y31z
745839
745800
2025-07-02T20:47:49Z
Mewtow
31375
/* Les microarchitectures K5 et K6 d'AMD */
745839
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction.
Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10.
La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée.
Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable.
L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs.
Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''.
La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]]
====Les microarchitectures K5 et K6 d'AMD====
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''.
Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc.
Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
====Les microarchitectures K7, K8 et K10 d'AMD====
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
lmdrhsvd1b50bteycf5973nepkokxjr
745840
745839
2025-07-02T20:48:13Z
Mewtow
31375
/* Les microarchitectures K5 et K6 d'AMD */
745840
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multicycle s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
===Les circuits hors-ALU sont soit dupliqués, soit adaptés===
Un processeur superscalaire doit être capable de : lire plusieurs instructions depuis la mémoire, les décoder, renommer leurs registres, et les envoyer à l'unité d'émission. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de renommage, les unités de calcul, le ROB, etc. Dans les faits, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres. Voici ce que cela donne dans les grandes lignes.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Deux solutions pour cela. La première est de doubler la taille du bus connecté au cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc. Mais tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Une autre solution est d'utiliser un cache d'instruction multiport. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore plus complexe, comme on le verra plus bas.
Il doit ensuite décoder plusieurs instructions en même temps. Il se trouve que les dépendances entre instruction ne posent pas de problème pour le décodeur. Rien ne s'oppose à ce qu'on utilise plusieurs décodeurs séparés. Il faut dire que les décodeurs sont de purs circuits combinatoires, du moins sur les processeurs avec une file de micro-opérations, ou avec une fenêtre d'instruction, ou des stations de réservation.
Par contre, l'unité de renommage de registre n'est pas dupliquée, mais adaptées, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
L'unité d'émission, la file de micro-opération et tous les circuits liés aux dépendances d'instruction ne sont pas dupliqués, car il peut y avoir des dépendances entre instruction chargées simultanément. L'unité d'émission est modifiée de manière à émettre plusieurs instructions à la fois. Si c'est un ''scoreboard'', il doit être modifié pour détecter les dépendances entre les instructions à émettre. Si c'est une fenêtre d'instruction ou une station de réservation, les choses sont plus simples, il faut basiquement rajouter des ports de lecture et écriture pour insérer et émettre plusieurs instructions à la fois, et modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps.
===La duplication des unités de calcul et les contraintes d’appariement===
Pour émettre plusieurs instructions en même temps, encore faut-il avoir de quoi les exécuter. En clair : un processeur superscalaire doit avoir plusieurs unités de calcul séparées. Les processeurs avec un pipeline dynamique incorporent plusieurs unités pour les instructions entières, une unité pour les instructions flottantes, une unité pour les accès mémoire et éventuellement une unité pour les tests/branchements. Au lieu de parler d'unités de calcul, un terme plus correct serait le terme d''''avals''', que nous avions introduit dans le chapitre sur les pipelines dynamiques, mais n'avons pas eu l'occasion d'utiliser par la suite.
Les processeurs avec un pipeline dynamique incorporent déjà plusieurs avals, mais chaque aval ne peut accepter qu'une nouvelle instruction par cycle. Et cela ne change pas sur les processeurs superscalaire, une unité de calcul reste une unité de calcul. Il y a plusieurs manières de gérer les avals sur un processeur superscalaire. La première duplique tous les avals : toutes les unités de calcul sont dupliquées. Par exemple, prenons un processeur simple-émission et transformons-le en processeur à double émission. Intuitivement, on se dit qu'il faut dupliquer toutes les unités de calcul. Si le processeur de base a une ALU entière, une FPU et un circuit multiplieur, ils sont tous dupliqués.
L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Tant que les deux instructions n'ont pas de dépendances de données, il peut les émettre. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer tous les circuits. Typiquement, les ALU simples sont dupliquées, de même que les unités de calcul d'adresse, mais la FPU et les circuits multiplieurs ne sont pas dupliqués. En faisant ainsi, le cout en transistors est grandement réduire. Par contre, cela entraine l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur, idem avec deux additions flottantes si l'additionneur flottant n'est pas dupliqué. La conséquence est que les processeurs superscalaires ont des contraintes sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a donc des paires d'instructions autorisées et des paires interdites.
Par exemple, l'exécution simultanée de deux branchements est interdite. Les branchements sont sérialisés, exécutés l'un après l'autre. Il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. Mais il n'est souvent pas possible d'émettre deux branchements en même temps. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
==L'étape de chargement superscalaire==
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est dnc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes.
La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Mais laissons de côté cette difficulté et passons au vrai problème. Charger un gros bloc de mémoire permet de charger plusieurs instructions, mais il y a potentiellement des branchements dans le bloc. Et on doit gérer le cas où ils sont pris, le cas où les instructions suivantes dans le bloc doivent être annulées. En clair, il faut détecter les branchements dans le bloc chargé et gérer le cas où ils sont pris.
: Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base (''basic block'').
===Le circuit de fusion de blocs===
Les processeurs superscalaires simples ne se préoccupent pas des branchements lors du chargement. Les instructions chargées en même temps sont toutes décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Les branchements sont donc prédits comme étant non-pris systématiquement. Mais d'autres sont plus malins et utilisent la prédiction de branchement pour savoir si un branchement est pris ou non.
Partons du principe que le branchement est pris : le processeur doit charger toutes les instructions d'un bloc, sauf celles qui suivent le branchement pris. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). Le résultat en sortie du fusionneur est ce qu'on appelle une '''trace'''.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Le principe peut se généraliser si un bloc contient plusieurs branchements pris, avec un nombre de blocs supérieur à deux. Mais cela demande une unité de prédiction de branchement capable de prédire plusieurs branchements par cycle.
===Le cache de traces===
Si jamais un bloc est rechargé et que ses branchements sont pris à l'identique, le résultat du fusionneur sera le même. Il est intéressant de conserver cette trace dans un '''cache de traces''' pour la réutiliser ultérieurement. Le cache de trace n'a été utilisé que sur un seul processeur commercial : le Pentium 4 d'Intel. Fait intéressant, son cache de trace ne mémorisait pas des suites d'instructions, mais des suites de micro-opérations. En clair, il mémorisait des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Ce qui explique que le temps d'accès au cache de trace n'était pas un problème, même s'il était comparable au temps d'accès du cache d'instruction. Il a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
[[File:BasicBlocks.png|vignette|Blocs de base.]]
Pour comprendre ce qu'est une trace, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D. Un cache de trace idéal mémorise les deux traces ABC et ABD dans deux lignes de cache séparées. Il peut mémoriser des traces différentes, même si leur début est le même.
Un cache de trace peut supporter des '''succès de cache de trace partiels'''. Prenez le cas où le processeur veut lire la trace ABC mais que le cache de trace ne contient que la trace ABD : c'est un succès partiel. Dans ce cas, le processeur peut lire les blocs de base A et B depuis le cache de trace, et lit D depuis le cache d'instruction. Et cela vaut dans le cas général : si le cache a mémorisé une trace similaire à celle demandée, dont seuls les premiers blocs de base correspondent, il peut lire les premiers blocs de base dans le cache de trace et lire le reste dans le cache d'instruction.
Il y a une certaine redondance dans le contenu du cache de trace, car certaines traces partagent des blocs de base. Pour éviter cela, il est possible de mémoriser les blocs de base dans des caches séparés et les assembler avec un fusionneur. Par exemple, au lieu d'utiliser un cache de traces unique, on va utiliser quatre '''caches de blocs de base''', suivi par un fusionneur qui reconstitue la trace. On économise du cache, au dépend d'un temps d'accès plus long vu qu'il faut reconstituer la trace.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir décoder plusieurs instructions à la fois. Ce n'est pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires. De plus, les unités de renommage et d'émission doivent être modifiées.
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient généralement plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type peut charger des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Ou alors, il se fera dans un seul décodeur qui pourra décoder plusieurs instructions par cycles.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblés. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais rien n'est encore implémenté (à ma connaissance). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément. De plus, il doit détecter les paires d'instructions interdites et les sérialiser. Autant dire que ce n'est pas très pratique. La détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'en suit. La différence principale est liée au banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est proportionnel à la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, cela ne change rien. Le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire ou écrire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, le banc de registre doit être relié à toutes les unités de calcul en même temps, ce qui fait 2 ports de lecture par unité de calcul. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Et plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions : l'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’arbitre du banc de registres (''register file arbiter'').
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, précédées par une fenêtre d'instruction avec N ports d'émission. Un tel processeur peut émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception.
La raison est que les processeurs superscalaires usuels sont conçus à partir d'un processeur à pipeline dynamique normal, qu'ils améliorent pour le rendre superscalaire. Les processeurs avec un pipeline dynamique incorporent plusieurs unités de calcul distinctes, avec une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et éventuellement une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, les unités de calcul existantes sont connectées à des ports d'émission différents.
===La double émission entière-flottante===
En théorie, il est possible d'imaginer un CPU superscalaire sans dupliquer les ALU, en se contenant d'exploiter au mieux les unités de calcul existantes. Prenons le cas d'un processeur avec une ALU entière et une ALU flottante, qu'on veut transformer en processeur superscalaire. L'idée est alors de permettre d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
L'idée est simple, la mise en œuvre utilise assez peu de circuits pour un gain en performance qui est faible, mais en vaut la peine. La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
Cependant, la méthode a un léger défaut. Une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent. Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Et c'est le cas pour la totalité des processeurs à exécution dans le désordre. Mais sur les processeurs à exécution dans l'ordre, si le budget en transistors est limité, les techniques de ce genre sont absentes.
En théorie, sans tampon de ré-ordonnancement ou équivalent, on ne peut pas émettre d'instruction flottante en même temps qu'une instruction entière à cause de ce problème d'exceptions flottantes. Le problème s'est manifesté sur les processeurs Atom d'Intel, et les concepteurs du processeur ont trouvé une parade. L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne normalement. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
Il faut noter que la double émission entière-flottante peut aussi être adaptée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières. L'avantage est que l'on peut alors utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération entière, une micro-opération flottante, et une micro-opération mémoire. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour émettre plusieurs micro-opérations entières en même temps. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', une unité de manipulation de bit. Les 5 unités permettent d'émettre 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare. En conséquence, les ports d'émission seront sous-utilisés, sous celui pour l'ALU entière.
Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission. Typiquement, il y a un port d'émission pour le multiplieur et un autre port d'émission pour le reste. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Mais on ne peut pas émettre/exécuter en parallèle des instructions simples. Il n'est pas exemple pas possible de faire un décalage en même temps qu'une addition. Le cout en performance est le prix à payer pour la réduction du nombre de ports d'émission, et il est assez faible.
: Je dis exécuter/émettre, car ces instructions s'exécutent en un cycle. Si elles ne sont pas émises en même temps, elles ne s'exécutent pas en même temps.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission.
: Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle est composée d'une unité de calcul d'adresse, éventuellement suivie d'une ''Load-store queue'' ou d'une autre structure qui remet en ordre les accès mémoire. Avec une unité mémoire de ce type, il n'est possible que d'émettre une seule micro-opération mémoire par cycle. Il n'y a pas émission multiple des accès mémoire.
Par contre, les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultannément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. On parle alors d''''émission multiple des accès mémoire'''. Les processeurs de ce type n'ont toujours qu'une seule ''Load-store queue'', mais elle est rendue multi-port afin de gérer plusieurs micro-opérations mémoire simultanés. De plus, les unités de calcul d'adresse sont dupliquées pour gérer plusieurs calculs d'adresse simultanés. Chaque unité de calcul est alors reliée à un port d'émission.
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. A l'inverse, d'autres processeurs peuvent émettre plusieurs accès mémoire, avec toutes les combinaisons de lecture/écriture possibles.
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 trois ports d'émission reliés à l'unité mémoire. Un port pour les lectures, deux pour les écritures. Le premier port d'écriture recevait la donnée à écrire dans le cache, le second s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et d'un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
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.
[[File:Athlon.png|centre|vignette|upright=2.5|Athlon]]
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres). Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant. Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant. Le troisième alimente une ALU entière, sans circuit flottant dédié.
[[File:Intel Core2 arch.svg|centre|vignette|upright=2.5|Intel Core 2 Duo - microarchitecture.]]
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnections entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==Un étude des microarchitectures superscalaires x86 d'Intel==
Après avoir vu beaucoup de théorie, voyons maintenant comment les microarchitectures Intel et AMD ont implémenté l'exécution superscalaire. Nous allons nous concentrer sur les processeurs Intel pour une raison simple : il y a plus de schémas disponibles sur wikicommons, ce qui me facilite le travail.
===Les processeurs Atom d'Intel, de microarchitecture Bonnell===
Les processeurs Atom sont des processeurs basse consommation produits et conçus par Intel. Il regroupent des processeurs de microarchitecture très différentes. La toute première microarchitecture ATOM était la microarchitecture Bonnell, qui est de loin la plus simple à étudier. Il s'agissait de processeurs sans exécution dans le désordre, sans renommage de registres. De nos jours, de tels processeurs ont disparus, même pour les processeurs basse consommation, mais les premiers processeurs Atom étaient dans ce cas. Mais le processeur était superscalaire et pouvait émettre deux instructions simultanées. Son pipeline faisait 16 étages, ce qui est beaucoup.
L'architecture est assez simple. Premièrement, le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction. La file d'instruction est alors reliée à deux décodeurs, ce qui permet de décoder deux instructions en même temps. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le chemin de données est conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille.
La file de micro-opérations est suivie par deux ports d'exécution, avec chacun leur logique d'émission. Les deux ports peuvent émettre au maximum 2 µops par cycle. Le résultat est que deux instructions consécutives peuvent s'exécuter, chacune dans deux avals séparés, dans deux pipelines différents. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Les deux ports sont capables de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Pour cela, ils ont chacun une ALU simple dédiée. Mais cela s'arrête là. Le second port est optimisé pour les opérations de type ''load-up''. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières. Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA.
Comme on le voit, la séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations. Et cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante.
Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Chose étonnante pour un processeur superscalaire, il n'utilisait pas de renommage de registre ni d’exécution dans le désordre. Vous avez bien lu : la superscalarité est apparue dans les processeurs commerciaux avant l'exécution dans le désordre.
Le Pentium 1 était un processeur à double émission, qui disposait de deux pipelines nommés U et V. On pouvait en tirer parti lorsque deux instructions consécutives pouvaient être exécutées en parallèles, si elles n'avaient pas de dépendances. Chose peu courante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX.
Les deux pipelines disposaient d'une unité de calcul entière identiques dans les deux pipelines. Le pipeline U incorporait un circuit multiplieur/diviseur et un ''barrel shifter'' pour les décalages/rotations. Les deux pipelines avaient chacun uen unité de calcul d'adresse, mais elle n'étaient pas identique : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse. La FPU était dans le pipeline U, de même que l'unité MMX.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| colspan="2" | ALU simple (une par pipeline)
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux ALU géraient les opérations bit à bit, les additions et soustractions, et les comparaisons (qui sont des soustractions déguisées). En conséquence, les instructions suivantes étaient exécutables dans les deux pipelines, ce qui fait qu'on pouvait en faire deux en même temps :
* l'instruction MOV, dépend du mode d'adressage ;
* les instructions de gestion de la pile PUSH et POP, dépend du mode d'adressage ;
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction NOP, qui ne fait rien.
Les instructions suivantes sont exécutables seulement dans le pipeline U : les calculs d'adresse autres que LEA, les décalages et rotations, la multiplication et la division, les opérations flottantes. Il faut noter qu'il y a cependant quelques restrictions. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V en parallèle. Dans le même genre, les branchements sont exécutables dans les deux pipelines, mais on ne peut exécuter une autre opération en parallèle qu'à la condition que le branchement soit exécuté dans le pipeline V.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
===La microarchitecture P6 du Pentium 2/3===
La microachitecture suivante, nommée P6, était une microarchitecture plus élaborée. Le pipeline faisait 12 étages, dont seuls les trois derniers correspondent au chemin de données. Il s'agissait du premier processeur à exécution dans le désordre de la marque, avec une implémentation basée sur des stations de réservation. Il gérait aussi le renommage de registre, avec un renommage de registre dans le ROB, commandé par une table d'alias.
[[File:Intel Pentium Pro Microarchitecture Block Diagram.svg|centre|vignette|upright=2|Intel Pentium Pro Microarchitecture Block Diagram]]
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode. Le processeur était à doubvle émission, du fait que les deux décodeurs simples faisaient le gros du travail, et passaient la main au décodeur microcodé quand aucune instruction ne leur était attribué.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
===La microarchitecture Netburst du Pentium 4===
La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm). Les deux premières avaient un pipeline de 20 étages, les deux suivants avaient 32 étages ! Le grand nombre d'étages permettait d'avoir une fréquence très élevée, mais posait de nombreux problèmes. Vider le pipeline était très long et il fallait une prédiction de branchement au top pour réduire l'impact des mauvaises prédictions. L'unité de prédiction de branchement était une des plus élvoluées pour l'époque. Pour l'époque.
Il dispose d'un cache de trace et a été le seul processeur commercial à en utiliser un. Niveau décodeurs, on retrouve le décodeur lent à base de microcode présent sur les anciennes versions, couplé à un décodeur simple. L'unité de renomage utilise une table d'alias. Le renommage de registres se fait avec un banc de registres physiques. Vous pouvez remarquer dans le schéma suivant la présence de deux files de micro-opérations : une pour les accès mémoire, l'autre pour les autres opérations. Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation.
Niveau ports d'émission, il y a quatre ports. Un pour les lectures mémoire, un pour les écriture mémoire, et deux autres qui mélangent FPU et ALUs. Le premier port est relié à une ALU complexe et une FPU spécialisée dans les MOV flottants. Le second port est relié à tout le reste : ALU basique ''barrel shifter'', FPU. Fait amusant, les ALU entières étaient cadencées à une fréquence double de celle du processeur, ce qui fait que les files d'émissionsont aussi censées l'être, de même que les ''scoreboard''. Sauf qu'en réalité, les circuits étaient dupliqués : l'un allait à une fréquence double, l'autre allait à la fréquence normale.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures AMD ont beaucoup évoluées avec le temps. Dans ce qui va suivre, nous allons les voir dans l'ordre chronologique, en partant de l'architecture K5, pour arriver aux dernières architectures Zen. Nous allons voir que les architectures AMD ont évoluées progressivement, chacune améliorant la précédente en faisant des rajouts ou modifications mineures, à l'exception de quelques grandes cassures dans la continuité où AMD a revu sa copie de fond en comble. L'architecture Bulldozer a été une première cassure, suivie par l'introduction des architectures Zen.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction.
Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10.
La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée.
Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable.
L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs.
Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''.
La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]]
====Les microarchitectures K5 et K6 d'AMD====
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''.
Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc.
Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K6
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
====Les microarchitectures K7, K8 et K10 d'AMD====
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
Par contre, la manière d'alimenter ces ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Elle avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opération mémoire. La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations.
Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
La ''Load-Store Queue'' peut mémoriser 44 lectures/écritures, avec cependant une petite nuance. Parmi les 44 lectures/écritures, 12 sont réservées au cache L1 et 32 le sont pour le cache L2. En réalité, il y a deux ''LSQ'', une pour le cache L1 qui fait 12 entrées, une seconde pour le L2 qui fait 32 entrées.
[[File:Athlon arch.png|centre|vignette|upright=3|Microarchitecture K7 d'AMD.]]
Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
Niveau micro-opérations entières, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, une par ALU entière, de 8 micro-opérations chacune. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU. Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
[[File:AMD Grayhound microarchitecture.png|centre|vignette|upright=3|Microarchitecture K8 et K10 d'AMD.]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. La microarchitecture Zen 1 est illustrée ci-dessous. Le passage à Zen 2 a ajouté une unité de calcul d'adresse (4 ALU / 3 AGU), le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire au niveau du cache
| prevText=Le parallélisme mémoire au niveau du cache
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
rz5hvbu37mh6qqqiu68batauxups47a
Fonctionnement d'un ordinateur/Les mémoires cache
0
65957
745777
745222
2025-07-02T17:54:38Z
Mewtow
31375
/* La communication du cache d'instruction avec le séquenceur */
745777
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
Sur certains processeurs, il y a deux caches L1 séparés : un '''cache d'instructions''', dédié aux instructions, et un autre pour les données. Les deux caches sont reliés au reste du processeur, ainsi qu'au cache L2. Pour les liaisons avec le processeur proprement dit, il y a un bus séparé pour le cache d'instruction et un autre pour le cache de données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. C'est théoriquement possible avec un cache L2 multiport, mais l'usage de caches séparés est plus simple. Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
===Pourquoi séparer instructions et données dans des caches séparés ?===
En soi, le fait de dédier un cache séparé pour les instructions est assez logique, vu que données et instructions sont deux choses radicalement différentes. La différence principale est que, comparé aux données, les instructions ont tendance à avoir une bonne localité spatiale et temporelle.
Localité spatiale tout d'abord parce que des instructions consécutives se suivent en mémoire. Les branchements sont certes à l'origine de sauts dans le programme, mais la plupart sautent à un endroit très proche, seuls les appels de fonction et appels systèmes brisent la localité spatiale. Par contre, les données ont une localité moins bonne. Il faut dire que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire comme le sont les instructions consécutives. De plus, les instructions sont statiques, alors que les données sont dynamiques. Les données d'un programme changent beaucoup dans le temps, alors que les instructions sont presque tout le temps immuables (le code auto-modifiant est très rare de nos jours).
Pour ce qui est de la localité temporelle, elle est très variable pour les données. Mais pour les instructions, elle est plus courante. Les boucles sont évidemment une source de localité temporelle, au même titre que les fonctions dans une moindre mesure (une fonction est exécutée plusieurs fois dans un programme, bien qu'il se passe un certain temps entre les deux). Et elles sont très fréquentes dans un code, que ce soit en termes de nombres d'instructions en mémoire qu'en nombre d'instructions exécutées.
C'est aussi la raison pour laquelle, sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. Et il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. La raison est que ces processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif. Par contre, un cache d’instruction fonctionne parfaitement, les programmes exécutés ayant une bonne localité, aussi bien temporelle que spatiale.
Les conséquences sont multiples : les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Pour donner un exemple : les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
===Les avantages et inconvénients des caches d'instructions===
Les arguments précédents justifient que l'on puisse dédier un cache aux instructions. Cependant, ces arguments sont valables à tous les niveaux de la hiérarchie mémoire, y compris au niveau du cache L2 et L3, qui sont eux unifiés. On n'a pas de cache L2 dédié aux instructions ou aux données, mais un cache L2 unique pour les deux. Comment expliquer alors que la spécialisation se fasse spécifiquement au niveau du cache L1 ? La raison est que les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Les caches L1 et L2/L3 ont des usages différents : cache petit mais rapide pour le L1, gros et lent pour le L2/L3. Et ces contraintes sont déterminantes pour décider si tel ou tel niveau de cache est séparé en deux caches spécialisés ou non.
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs disposant d'un cache avaient un cache unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes. N'oublions pas que les concepteurs de processeurs sont limités en transistors et doivent faire des choix. Les transistors utilisés pour le cache d'instruction auraient pu être utilisés pour autre chose, comme augmenter la capacité des caches existants, et notamment le cache L1. Ajouter un cache d'instruction demande de faire des choix, de bien peser le pour et le contre, de bien juger des avantages et inconvénients d'un cache d'instruction.
Le premier compromis à faire est celui entre capacité des caches et performances, plus précisément entre le temps d'accès et la capacité totale du cache L1. Pour faire simple, on a le choix entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. Cependant, cela vient avec un défaut qui réduit la capacité effective. Par exemple, pour un cache d'une capacité de 64 kibioctets, on peut décider de réserver 10 kb aux instructions et le reste aux données, ou encore 40 Kb aux instructions, etc. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 Kb et un cache de données de 32 Kb, impossible d'allouer 40 Kb aux données et 20 aux instructions : le cache de données est trop petit. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre. Et cela explique en grande partie pour seul le cache L1 est séparé en deux : c'est le temps d'accès qui prime pour le cache L1, alors que la capacité effective prime pour les niveaux L2 et au-delà.
===La communication du cache d'instruction avec le séquenceur===
Une autre différence entre instructions et données est la suivante : les instructions sont utilisées par le séquenceur et les données par le chemin de données. Et cela se marie bien avec deux caches séparés, placés à des endroits très différents du processeur. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Il est parfois intégré à l'unité de chargement, par simplicité de conception du processeur. Quant au cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés. Pour simplifier, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une architecture Harvard modifiée. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
Une telle organisation facilite l'implémentation de certaines optimisations. Citons comme exemple, la technique dite du '''prédécodage''', qui accélère le décodage des instructions. Lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction.Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée.
Le prédécodage est surtout utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
===Le cache d'instruction est souvent en lecture seule===
Un point important est que les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===L'usage d'un cache L1 unique demande d'utiliser un cache multiport===
En théorie, on pourrait utiliser un cache L1 unique et le relier à la fois au séquenceur et au chemin de données. Mais utiliser un seul cache unifié demanderait un effort de câblage assez important, le cache devant être à la fois proche du séquenceur et du chemin de données. Les connexions entre le cache L1 unifié et le reste du processeur sont donc assez longues, tortueuses, et difficiles à câbler. De plus, ces longues connexions font que le transfert des bits prend plus de temps pour traverser le fil en longueur, ce qui pose des problèmes à haute fréquence. Avec deux caches séparés, on n'a pas ce problème, ce qui permet de garder des caches L1 très rapides. La lenteur et les problèmes de connexion sont reportés aux connexions entre les caches L1 et le cache L2, mais celui-ci accepte des temps d'accès plus longs.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Mais cet avantage peut s'obtenir avec un cache L1 unique, en utilisant un cache multiport, avec un port relié au séquenceur et un autre au chemin de données. Et le choix entre les deux n'est pas évident. Les caches multiports sont clairement une solution viable : les caches L2 et L3 sont tous des caches multiports. Là encore, tout est histoire de compromis : les mémoires multiport sont plus lentes, plus grosses, plus compliquées à fabriquer. L'impact en termes de temps d'accès est en faveur de la mémoire simple port, tout comme la simplicité de conception. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. Pour les caches L1 de petite capacité, le temps d'accès est très important, ce qui favorise les caches séparés. De plus, utiliser deux caches séparés n'a pas trop d'impact sur le budget en transistors, car les caches L1 sont petits. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que l'économie en circuits est significative.
Et cette histoire de cache simple ou multiport est de plus en plus contraignante. Les processeurs modernes sont capables d’exécuter plusieurs instructions en parallèle, comme on le verra dans quelques chapitres. Et la conséquence est que les caches L1 doivent être capables de lire/écrire plusieurs données en même temps, tout en chargeant plusieurs instructions simultanément. Les deux caches L doivent donc être multiports tous les deux. Le choix est donc entre deux caches avec chacun un nombre limité de ports, ou un cache unique avec beaucoup de ports. S'il fallait utiliser un cache unique, celui-ci aurait au moins une dizaine de ports, voire plus, ce qui serait impraticable. Les concepteurs de processeurs se facilitent la vie en utilisant deux caches séparés avec peu de ports. Mais le fond du compromis est le même : soit un cache rapide avec peu de ports, soit un cache plus lent avec beaucoup de ports.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
ml88r9oaq77p058drxrd1w1zqggb9iy
745778
745777
2025-07-02T18:01:20Z
Mewtow
31375
/* Un exemple de cache : le cache d'instruction */
745778
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. C'est théoriquement possible avec un cache L2 multiport, mais l'usage de caches séparés est plus simple.
===Pourquoi séparer instructions et données dans des caches séparés ?===
En soi, le fait de dédier un cache séparé pour les instructions est assez logique, vu que données et instructions sont deux choses radicalement différentes. La différence principale est que, comparé aux données, les instructions ont tendance à avoir une bonne localité spatiale et temporelle.
Localité spatiale tout d'abord parce que des instructions consécutives se suivent en mémoire, même si la présence de branchements nuance ces affirmations. Heureusement, la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction et appels systèmes brisent la localité spatiale. Par contre, rien ne garantit que des données utilisées ensemble soient regroupées en mémoire, comme le sont les instructions consécutives. Pour ce qui est de la localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions (une fonction est exécutée plusieurs fois dans un programme, bien qu'il se passe un certain temps entre les deux).
C'est la raison pour laquelle, sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. Et il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. La raison est que ces processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
Les conséquences sont multiples : les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Pour donner un exemple : les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
===Les avantages et inconvénients des caches d'instructions===
Les arguments précédents justifient que l'on puisse dédier un cache aux instructions. Cependant, ces arguments sont valables à tous les niveaux de la hiérarchie mémoire, y compris au niveau du cache L2 et L3, qui sont eux unifiés. On n'a pas de cache L2 dédié aux instructions ou aux données, mais un cache L2 unique pour les deux. Comment expliquer alors que la spécialisation se fasse spécifiquement au niveau du cache L1 ? La raison est que les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Les caches L1 et L2/L3 ont des usages différents : cache petit mais rapide pour le L1, gros et lent pour le L2/L3. Et ces contraintes sont déterminantes pour décider si tel ou tel niveau de cache est séparé en deux caches spécialisés ou non.
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs disposant d'un cache avaient un cache unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes. N'oublions pas que les concepteurs de processeurs sont limités en transistors et doivent faire des choix. Les transistors utilisés pour le cache d'instruction auraient pu être utilisés pour autre chose, comme augmenter la capacité des caches existants, et notamment le cache L1. Ajouter un cache d'instruction demande de faire des choix, de bien peser le pour et le contre, de bien juger des avantages et inconvénients d'un cache d'instruction.
Le premier compromis à faire est celui entre capacité des caches et performances, plus précisément entre le temps d'accès et la capacité totale du cache L1. Pour faire simple, on a le choix entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. Cependant, cela vient avec un défaut qui réduit la capacité effective. Par exemple, pour un cache d'une capacité de 64 kibioctets, on peut décider de réserver 10 kb aux instructions et le reste aux données, ou encore 40 Kb aux instructions, etc. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 Kb et un cache de données de 32 Kb, impossible d'allouer 40 Kb aux données et 20 aux instructions : le cache de données est trop petit. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre. Et cela explique en grande partie pour seul le cache L1 est séparé en deux : c'est le temps d'accès qui prime pour le cache L1, alors que la capacité effective prime pour les niveaux L2 et au-delà.
===La communication du cache d'instruction avec le séquenceur===
Une autre différence entre instructions et données est la suivante : les instructions sont utilisées par le séquenceur et les données par le chemin de données. Et cela se marie bien avec deux caches séparés, placés à des endroits très différents du processeur. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Il est parfois intégré à l'unité de chargement, par simplicité de conception du processeur. Quant au cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés. Pour simplifier, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une architecture Harvard modifiée. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
Une telle organisation facilite l'implémentation de certaines optimisations. Citons comme exemple, la technique dite du '''prédécodage''', qui accélère le décodage des instructions. Lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction.Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée.
Le prédécodage est surtout utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
===Le cache d'instruction est souvent en lecture seule===
Un point important est que les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===L'usage d'un cache L1 unique demande d'utiliser un cache multiport===
En théorie, on pourrait utiliser un cache L1 unique et le relier à la fois au séquenceur et au chemin de données. Mais utiliser un seul cache unifié demanderait un effort de câblage assez important, le cache devant être à la fois proche du séquenceur et du chemin de données. Les connexions entre le cache L1 unifié et le reste du processeur sont donc assez longues, tortueuses, et difficiles à câbler. De plus, ces longues connexions font que le transfert des bits prend plus de temps pour traverser le fil en longueur, ce qui pose des problèmes à haute fréquence. Avec deux caches séparés, on n'a pas ce problème, ce qui permet de garder des caches L1 très rapides. La lenteur et les problèmes de connexion sont reportés aux connexions entre les caches L1 et le cache L2, mais celui-ci accepte des temps d'accès plus longs.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Mais cet avantage peut s'obtenir avec un cache L1 unique, en utilisant un cache multiport, avec un port relié au séquenceur et un autre au chemin de données. Et le choix entre les deux n'est pas évident. Les caches multiports sont clairement une solution viable : les caches L2 et L3 sont tous des caches multiports. Là encore, tout est histoire de compromis : les mémoires multiport sont plus lentes, plus grosses, plus compliquées à fabriquer. L'impact en termes de temps d'accès est en faveur de la mémoire simple port, tout comme la simplicité de conception. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. Pour les caches L1 de petite capacité, le temps d'accès est très important, ce qui favorise les caches séparés. De plus, utiliser deux caches séparés n'a pas trop d'impact sur le budget en transistors, car les caches L1 sont petits. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que l'économie en circuits est significative.
Et cette histoire de cache simple ou multiport est de plus en plus contraignante. Les processeurs modernes sont capables d’exécuter plusieurs instructions en parallèle, comme on le verra dans quelques chapitres. Et la conséquence est que les caches L1 doivent être capables de lire/écrire plusieurs données en même temps, tout en chargeant plusieurs instructions simultanément. Les deux caches L doivent donc être multiports tous les deux. Le choix est donc entre deux caches avec chacun un nombre limité de ports, ou un cache unique avec beaucoup de ports. S'il fallait utiliser un cache unique, celui-ci aurait au moins une dizaine de ports, voire plus, ce qui serait impraticable. Les concepteurs de processeurs se facilitent la vie en utilisant deux caches séparés avec peu de ports. Mais le fond du compromis est le même : soit un cache rapide avec peu de ports, soit un cache plus lent avec beaucoup de ports.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
tm2cr4atq55x0ub6q2f6487vpfcu0hg
745779
745778
2025-07-02T18:03:02Z
Mewtow
31375
/* Pourquoi séparer instructions et données dans des caches séparés ? */
745779
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. C'est théoriquement possible avec un cache L2 multiport, mais l'usage de caches séparés est plus simple.
===Pourquoi séparer instructions et données dans des caches séparés ?===
En soi, le fait de dédier un cache séparé pour les instructions est assez logique, vu que données et instructions sont deux choses radicalement différentes. La différence principale est que, comparé aux données, les instructions ont tendance à avoir une bonne localité spatiale et temporelle.
Localité spatiale tout d'abord parce que des instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. La présence de branchements nuance ces affirmations, si ce n'est que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale. Pour ce qui est de la localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions (une fonction est exécutée plusieurs fois dans un programme, bien qu'il se passe un certain temps entre les deux).
C'est la raison pour laquelle, sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. Et il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. La raison est que ces processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
De plus, les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Pour donner un exemple : les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
===Les avantages et inconvénients des caches d'instructions===
Les arguments précédents justifient que l'on puisse dédier un cache aux instructions. Cependant, ces arguments sont valables à tous les niveaux de la hiérarchie mémoire, y compris au niveau du cache L2 et L3, qui sont eux unifiés. On n'a pas de cache L2 dédié aux instructions ou aux données, mais un cache L2 unique pour les deux. Comment expliquer alors que la spécialisation se fasse spécifiquement au niveau du cache L1 ? La raison est que les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Les caches L1 et L2/L3 ont des usages différents : cache petit mais rapide pour le L1, gros et lent pour le L2/L3. Et ces contraintes sont déterminantes pour décider si tel ou tel niveau de cache est séparé en deux caches spécialisés ou non.
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs disposant d'un cache avaient un cache unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes. N'oublions pas que les concepteurs de processeurs sont limités en transistors et doivent faire des choix. Les transistors utilisés pour le cache d'instruction auraient pu être utilisés pour autre chose, comme augmenter la capacité des caches existants, et notamment le cache L1. Ajouter un cache d'instruction demande de faire des choix, de bien peser le pour et le contre, de bien juger des avantages et inconvénients d'un cache d'instruction.
Le premier compromis à faire est celui entre capacité des caches et performances, plus précisément entre le temps d'accès et la capacité totale du cache L1. Pour faire simple, on a le choix entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. Cependant, cela vient avec un défaut qui réduit la capacité effective. Par exemple, pour un cache d'une capacité de 64 kibioctets, on peut décider de réserver 10 kb aux instructions et le reste aux données, ou encore 40 Kb aux instructions, etc. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 Kb et un cache de données de 32 Kb, impossible d'allouer 40 Kb aux données et 20 aux instructions : le cache de données est trop petit. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre. Et cela explique en grande partie pour seul le cache L1 est séparé en deux : c'est le temps d'accès qui prime pour le cache L1, alors que la capacité effective prime pour les niveaux L2 et au-delà.
===La communication du cache d'instruction avec le séquenceur===
Une autre différence entre instructions et données est la suivante : les instructions sont utilisées par le séquenceur et les données par le chemin de données. Et cela se marie bien avec deux caches séparés, placés à des endroits très différents du processeur. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Il est parfois intégré à l'unité de chargement, par simplicité de conception du processeur. Quant au cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés. Pour simplifier, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une architecture Harvard modifiée. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
Une telle organisation facilite l'implémentation de certaines optimisations. Citons comme exemple, la technique dite du '''prédécodage''', qui accélère le décodage des instructions. Lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction.Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée.
Le prédécodage est surtout utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
===Le cache d'instruction est souvent en lecture seule===
Un point important est que les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===L'usage d'un cache L1 unique demande d'utiliser un cache multiport===
En théorie, on pourrait utiliser un cache L1 unique et le relier à la fois au séquenceur et au chemin de données. Mais utiliser un seul cache unifié demanderait un effort de câblage assez important, le cache devant être à la fois proche du séquenceur et du chemin de données. Les connexions entre le cache L1 unifié et le reste du processeur sont donc assez longues, tortueuses, et difficiles à câbler. De plus, ces longues connexions font que le transfert des bits prend plus de temps pour traverser le fil en longueur, ce qui pose des problèmes à haute fréquence. Avec deux caches séparés, on n'a pas ce problème, ce qui permet de garder des caches L1 très rapides. La lenteur et les problèmes de connexion sont reportés aux connexions entre les caches L1 et le cache L2, mais celui-ci accepte des temps d'accès plus longs.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Mais cet avantage peut s'obtenir avec un cache L1 unique, en utilisant un cache multiport, avec un port relié au séquenceur et un autre au chemin de données. Et le choix entre les deux n'est pas évident. Les caches multiports sont clairement une solution viable : les caches L2 et L3 sont tous des caches multiports. Là encore, tout est histoire de compromis : les mémoires multiport sont plus lentes, plus grosses, plus compliquées à fabriquer. L'impact en termes de temps d'accès est en faveur de la mémoire simple port, tout comme la simplicité de conception. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. Pour les caches L1 de petite capacité, le temps d'accès est très important, ce qui favorise les caches séparés. De plus, utiliser deux caches séparés n'a pas trop d'impact sur le budget en transistors, car les caches L1 sont petits. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que l'économie en circuits est significative.
Et cette histoire de cache simple ou multiport est de plus en plus contraignante. Les processeurs modernes sont capables d’exécuter plusieurs instructions en parallèle, comme on le verra dans quelques chapitres. Et la conséquence est que les caches L1 doivent être capables de lire/écrire plusieurs données en même temps, tout en chargeant plusieurs instructions simultanément. Les deux caches L doivent donc être multiports tous les deux. Le choix est donc entre deux caches avec chacun un nombre limité de ports, ou un cache unique avec beaucoup de ports. S'il fallait utiliser un cache unique, celui-ci aurait au moins une dizaine de ports, voire plus, ce qui serait impraticable. Les concepteurs de processeurs se facilitent la vie en utilisant deux caches séparés avec peu de ports. Mais le fond du compromis est le même : soit un cache rapide avec peu de ports, soit un cache plus lent avec beaucoup de ports.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
o266768t61hnb7z5h2gw75jlbw1lnqm
745780
745779
2025-07-02T18:07:18Z
Mewtow
31375
/* Un exemple de cache : le cache d'instruction */
745780
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Une solution équivalente serait d'utiliser un cache L2 multiport, mais l'usage de caches séparés est plus simple.
===Pourquoi séparer instructions et données dans des caches séparés ?===
En soi, le fait de dédier un cache séparé pour les instructions est assez logique, vu que données et instructions sont deux choses radicalement différentes. La différence principale est que, comparé aux données, les instructions ont tendance à avoir une bonne localité spatiale et temporelle.
Localité spatiale tout d'abord parce que des instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. La présence de branchements nuance ces affirmations, si ce n'est que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale. Pour ce qui est de la localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions (une fonction est exécutée plusieurs fois dans un programme, bien qu'il se passe un certain temps entre les deux).
C'est la raison pour laquelle, sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. Et il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. La raison est que ces processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
De plus, les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Pour donner un exemple : les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
===Les avantages et inconvénients des caches d'instructions===
Les arguments précédents justifient que l'on puisse dédier un cache aux instructions. Cependant, ces arguments sont valables à tous les niveaux de la hiérarchie mémoire, y compris au niveau du cache L2 et L3, qui sont eux unifiés. On n'a pas de cache L2 dédié aux instructions ou aux données, mais un cache L2 unique pour les deux. Comment expliquer alors que la spécialisation se fasse spécifiquement au niveau du cache L1 ? La raison est que les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Les caches L1 et L2/L3 ont des usages différents : cache petit mais rapide pour le L1, gros et lent pour le L2/L3. Et ces contraintes sont déterminantes pour décider si tel ou tel niveau de cache est séparé en deux caches spécialisés ou non.
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs disposant d'un cache avaient un cache unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes. N'oublions pas que les concepteurs de processeurs sont limités en transistors et doivent faire des choix. Les transistors utilisés pour le cache d'instruction auraient pu être utilisés pour autre chose, comme augmenter la capacité des caches existants, et notamment le cache L1. Ajouter un cache d'instruction demande de faire des choix, de bien peser le pour et le contre, de bien juger des avantages et inconvénients d'un cache d'instruction.
Le premier compromis à faire est celui entre capacité des caches et performances, plus précisément entre le temps d'accès et la capacité totale du cache L1. Pour faire simple, on a le choix entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. Cependant, cela vient avec un défaut qui réduit la capacité effective. Par exemple, pour un cache d'une capacité de 64 kibioctets, on peut décider de réserver 10 kb aux instructions et le reste aux données, ou encore 40 Kb aux instructions, etc. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 Kb et un cache de données de 32 Kb, impossible d'allouer 40 Kb aux données et 20 aux instructions : le cache de données est trop petit. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre. Et cela explique en grande partie pour seul le cache L1 est séparé en deux : c'est le temps d'accès qui prime pour le cache L1, alors que la capacité effective prime pour les niveaux L2 et au-delà.
===La communication du cache d'instruction avec le séquenceur===
Une autre différence entre instructions et données est la suivante : les instructions sont utilisées par le séquenceur et les données par le chemin de données. Et cela se marie bien avec deux caches séparés, placés à des endroits très différents du processeur. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Il est parfois intégré à l'unité de chargement, par simplicité de conception du processeur. Quant au cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés. Pour simplifier, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une architecture Harvard modifiée. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
Une telle organisation facilite l'implémentation de certaines optimisations. Citons comme exemple, la technique dite du '''prédécodage''', qui accélère le décodage des instructions. Lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction.Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée.
Le prédécodage est surtout utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
===Le cache d'instruction est souvent en lecture seule===
Un point important est que les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===L'usage d'un cache L1 unique demande d'utiliser un cache multiport===
En théorie, on pourrait utiliser un cache L1 unique et le relier à la fois au séquenceur et au chemin de données. Mais utiliser un seul cache unifié demanderait un effort de câblage assez important, le cache devant être à la fois proche du séquenceur et du chemin de données. Les connexions entre le cache L1 unifié et le reste du processeur sont donc assez longues, tortueuses, et difficiles à câbler. De plus, ces longues connexions font que le transfert des bits prend plus de temps pour traverser le fil en longueur, ce qui pose des problèmes à haute fréquence. Avec deux caches séparés, on n'a pas ce problème, ce qui permet de garder des caches L1 très rapides. La lenteur et les problèmes de connexion sont reportés aux connexions entre les caches L1 et le cache L2, mais celui-ci accepte des temps d'accès plus longs.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Mais cet avantage peut s'obtenir avec un cache L1 unique, en utilisant un cache multiport, avec un port relié au séquenceur et un autre au chemin de données. Et le choix entre les deux n'est pas évident. Les caches multiports sont clairement une solution viable : les caches L2 et L3 sont tous des caches multiports. Là encore, tout est histoire de compromis : les mémoires multiport sont plus lentes, plus grosses, plus compliquées à fabriquer. L'impact en termes de temps d'accès est en faveur de la mémoire simple port, tout comme la simplicité de conception. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. Pour les caches L1 de petite capacité, le temps d'accès est très important, ce qui favorise les caches séparés. De plus, utiliser deux caches séparés n'a pas trop d'impact sur le budget en transistors, car les caches L1 sont petits. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que l'économie en circuits est significative.
Et cette histoire de cache simple ou multiport est de plus en plus contraignante. Les processeurs modernes sont capables d’exécuter plusieurs instructions en parallèle, comme on le verra dans quelques chapitres. Et la conséquence est que les caches L1 doivent être capables de lire/écrire plusieurs données en même temps, tout en chargeant plusieurs instructions simultanément. Les deux caches L doivent donc être multiports tous les deux. Le choix est donc entre deux caches avec chacun un nombre limité de ports, ou un cache unique avec beaucoup de ports. S'il fallait utiliser un cache unique, celui-ci aurait au moins une dizaine de ports, voire plus, ce qui serait impraticable. Les concepteurs de processeurs se facilitent la vie en utilisant deux caches séparés avec peu de ports. Mais le fond du compromis est le même : soit un cache rapide avec peu de ports, soit un cache plus lent avec beaucoup de ports.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
1aung2cuo0g3ghnsixjdzwqwurmpb3i
745781
745780
2025-07-02T18:10:32Z
Mewtow
31375
/* Pourquoi séparer instructions et données dans des caches séparés ? */
745781
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Une solution équivalente serait d'utiliser un cache L2 multiport, mais l'usage de caches séparés est plus simple.
===Pourquoi séparer instructions et données dans des caches séparés ?===
Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. En clair : quitte à choisir entre un cache d'instruction seul et un cache de donnée seul, mieux vaut prendre un cache d’instruction. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions.
: La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale.
D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
De plus, les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Pour donner un exemple : les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
===Les avantages et inconvénients des caches d'instructions===
Les arguments précédents justifient que l'on puisse dédier un cache aux instructions. Cependant, ces arguments sont valables à tous les niveaux de la hiérarchie mémoire, y compris au niveau du cache L2 et L3, qui sont eux unifiés. On n'a pas de cache L2 dédié aux instructions ou aux données, mais un cache L2 unique pour les deux. Comment expliquer alors que la spécialisation se fasse spécifiquement au niveau du cache L1 ? La raison est que les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Les caches L1 et L2/L3 ont des usages différents : cache petit mais rapide pour le L1, gros et lent pour le L2/L3. Et ces contraintes sont déterminantes pour décider si tel ou tel niveau de cache est séparé en deux caches spécialisés ou non.
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs disposant d'un cache avaient un cache unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes. N'oublions pas que les concepteurs de processeurs sont limités en transistors et doivent faire des choix. Les transistors utilisés pour le cache d'instruction auraient pu être utilisés pour autre chose, comme augmenter la capacité des caches existants, et notamment le cache L1. Ajouter un cache d'instruction demande de faire des choix, de bien peser le pour et le contre, de bien juger des avantages et inconvénients d'un cache d'instruction.
Le premier compromis à faire est celui entre capacité des caches et performances, plus précisément entre le temps d'accès et la capacité totale du cache L1. Pour faire simple, on a le choix entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. Cependant, cela vient avec un défaut qui réduit la capacité effective. Par exemple, pour un cache d'une capacité de 64 kibioctets, on peut décider de réserver 10 kb aux instructions et le reste aux données, ou encore 40 Kb aux instructions, etc. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 Kb et un cache de données de 32 Kb, impossible d'allouer 40 Kb aux données et 20 aux instructions : le cache de données est trop petit. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre. Et cela explique en grande partie pour seul le cache L1 est séparé en deux : c'est le temps d'accès qui prime pour le cache L1, alors que la capacité effective prime pour les niveaux L2 et au-delà.
===La communication du cache d'instruction avec le séquenceur===
Une autre différence entre instructions et données est la suivante : les instructions sont utilisées par le séquenceur et les données par le chemin de données. Et cela se marie bien avec deux caches séparés, placés à des endroits très différents du processeur. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Il est parfois intégré à l'unité de chargement, par simplicité de conception du processeur. Quant au cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés. Pour simplifier, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une architecture Harvard modifiée. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
Une telle organisation facilite l'implémentation de certaines optimisations. Citons comme exemple, la technique dite du '''prédécodage''', qui accélère le décodage des instructions. Lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction.Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée.
Le prédécodage est surtout utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
===Le cache d'instruction est souvent en lecture seule===
Un point important est que les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===L'usage d'un cache L1 unique demande d'utiliser un cache multiport===
En théorie, on pourrait utiliser un cache L1 unique et le relier à la fois au séquenceur et au chemin de données. Mais utiliser un seul cache unifié demanderait un effort de câblage assez important, le cache devant être à la fois proche du séquenceur et du chemin de données. Les connexions entre le cache L1 unifié et le reste du processeur sont donc assez longues, tortueuses, et difficiles à câbler. De plus, ces longues connexions font que le transfert des bits prend plus de temps pour traverser le fil en longueur, ce qui pose des problèmes à haute fréquence. Avec deux caches séparés, on n'a pas ce problème, ce qui permet de garder des caches L1 très rapides. La lenteur et les problèmes de connexion sont reportés aux connexions entre les caches L1 et le cache L2, mais celui-ci accepte des temps d'accès plus longs.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Mais cet avantage peut s'obtenir avec un cache L1 unique, en utilisant un cache multiport, avec un port relié au séquenceur et un autre au chemin de données. Et le choix entre les deux n'est pas évident. Les caches multiports sont clairement une solution viable : les caches L2 et L3 sont tous des caches multiports. Là encore, tout est histoire de compromis : les mémoires multiport sont plus lentes, plus grosses, plus compliquées à fabriquer. L'impact en termes de temps d'accès est en faveur de la mémoire simple port, tout comme la simplicité de conception. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. Pour les caches L1 de petite capacité, le temps d'accès est très important, ce qui favorise les caches séparés. De plus, utiliser deux caches séparés n'a pas trop d'impact sur le budget en transistors, car les caches L1 sont petits. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que l'économie en circuits est significative.
Et cette histoire de cache simple ou multiport est de plus en plus contraignante. Les processeurs modernes sont capables d’exécuter plusieurs instructions en parallèle, comme on le verra dans quelques chapitres. Et la conséquence est que les caches L1 doivent être capables de lire/écrire plusieurs données en même temps, tout en chargeant plusieurs instructions simultanément. Les deux caches L doivent donc être multiports tous les deux. Le choix est donc entre deux caches avec chacun un nombre limité de ports, ou un cache unique avec beaucoup de ports. S'il fallait utiliser un cache unique, celui-ci aurait au moins une dizaine de ports, voire plus, ce qui serait impraticable. Les concepteurs de processeurs se facilitent la vie en utilisant deux caches séparés avec peu de ports. Mais le fond du compromis est le même : soit un cache rapide avec peu de ports, soit un cache plus lent avec beaucoup de ports.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
0tymhvzzo5knr0uly1dvr0k1hpf6d6x
745782
745781
2025-07-02T18:10:51Z
Mewtow
31375
/* Pourquoi séparer instructions et données dans des caches séparés ? */
745782
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Une solution équivalente serait d'utiliser un cache L2 multiport, mais l'usage de caches séparés est plus simple.
===Pourquoi séparer instructions et données dans des caches séparés ?===
Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions.
: La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale.
D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
===Les avantages et inconvénients des caches d'instructions===
Les arguments précédents justifient que l'on puisse dédier un cache aux instructions. Cependant, ces arguments sont valables à tous les niveaux de la hiérarchie mémoire, y compris au niveau du cache L2 et L3, qui sont eux unifiés. On n'a pas de cache L2 dédié aux instructions ou aux données, mais un cache L2 unique pour les deux. Comment expliquer alors que la spécialisation se fasse spécifiquement au niveau du cache L1 ? La raison est que les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Les caches L1 et L2/L3 ont des usages différents : cache petit mais rapide pour le L1, gros et lent pour le L2/L3. Et ces contraintes sont déterminantes pour décider si tel ou tel niveau de cache est séparé en deux caches spécialisés ou non.
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs disposant d'un cache avaient un cache unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes. N'oublions pas que les concepteurs de processeurs sont limités en transistors et doivent faire des choix. Les transistors utilisés pour le cache d'instruction auraient pu être utilisés pour autre chose, comme augmenter la capacité des caches existants, et notamment le cache L1. Ajouter un cache d'instruction demande de faire des choix, de bien peser le pour et le contre, de bien juger des avantages et inconvénients d'un cache d'instruction.
Le premier compromis à faire est celui entre capacité des caches et performances, plus précisément entre le temps d'accès et la capacité totale du cache L1. Pour faire simple, on a le choix entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. Cependant, cela vient avec un défaut qui réduit la capacité effective. Par exemple, pour un cache d'une capacité de 64 kibioctets, on peut décider de réserver 10 kb aux instructions et le reste aux données, ou encore 40 Kb aux instructions, etc. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 Kb et un cache de données de 32 Kb, impossible d'allouer 40 Kb aux données et 20 aux instructions : le cache de données est trop petit. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre. Et cela explique en grande partie pour seul le cache L1 est séparé en deux : c'est le temps d'accès qui prime pour le cache L1, alors que la capacité effective prime pour les niveaux L2 et au-delà.
===La communication du cache d'instruction avec le séquenceur===
Une autre différence entre instructions et données est la suivante : les instructions sont utilisées par le séquenceur et les données par le chemin de données. Et cela se marie bien avec deux caches séparés, placés à des endroits très différents du processeur. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Il est parfois intégré à l'unité de chargement, par simplicité de conception du processeur. Quant au cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés. Pour simplifier, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une architecture Harvard modifiée. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
Une telle organisation facilite l'implémentation de certaines optimisations. Citons comme exemple, la technique dite du '''prédécodage''', qui accélère le décodage des instructions. Lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction.Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée.
Le prédécodage est surtout utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
===Le cache d'instruction est souvent en lecture seule===
Un point important est que les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===L'usage d'un cache L1 unique demande d'utiliser un cache multiport===
En théorie, on pourrait utiliser un cache L1 unique et le relier à la fois au séquenceur et au chemin de données. Mais utiliser un seul cache unifié demanderait un effort de câblage assez important, le cache devant être à la fois proche du séquenceur et du chemin de données. Les connexions entre le cache L1 unifié et le reste du processeur sont donc assez longues, tortueuses, et difficiles à câbler. De plus, ces longues connexions font que le transfert des bits prend plus de temps pour traverser le fil en longueur, ce qui pose des problèmes à haute fréquence. Avec deux caches séparés, on n'a pas ce problème, ce qui permet de garder des caches L1 très rapides. La lenteur et les problèmes de connexion sont reportés aux connexions entre les caches L1 et le cache L2, mais celui-ci accepte des temps d'accès plus longs.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Mais cet avantage peut s'obtenir avec un cache L1 unique, en utilisant un cache multiport, avec un port relié au séquenceur et un autre au chemin de données. Et le choix entre les deux n'est pas évident. Les caches multiports sont clairement une solution viable : les caches L2 et L3 sont tous des caches multiports. Là encore, tout est histoire de compromis : les mémoires multiport sont plus lentes, plus grosses, plus compliquées à fabriquer. L'impact en termes de temps d'accès est en faveur de la mémoire simple port, tout comme la simplicité de conception. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. Pour les caches L1 de petite capacité, le temps d'accès est très important, ce qui favorise les caches séparés. De plus, utiliser deux caches séparés n'a pas trop d'impact sur le budget en transistors, car les caches L1 sont petits. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que l'économie en circuits est significative.
Et cette histoire de cache simple ou multiport est de plus en plus contraignante. Les processeurs modernes sont capables d’exécuter plusieurs instructions en parallèle, comme on le verra dans quelques chapitres. Et la conséquence est que les caches L1 doivent être capables de lire/écrire plusieurs données en même temps, tout en chargeant plusieurs instructions simultanément. Les deux caches L doivent donc être multiports tous les deux. Le choix est donc entre deux caches avec chacun un nombre limité de ports, ou un cache unique avec beaucoup de ports. S'il fallait utiliser un cache unique, celui-ci aurait au moins une dizaine de ports, voire plus, ce qui serait impraticable. Les concepteurs de processeurs se facilitent la vie en utilisant deux caches séparés avec peu de ports. Mais le fond du compromis est le même : soit un cache rapide avec peu de ports, soit un cache plus lent avec beaucoup de ports.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
5z9jbsxp9dw0wwxlm1cv2hjgp9qg9fy
745783
745782
2025-07-02T18:11:24Z
Mewtow
31375
/* Pourquoi séparer instructions et données dans des caches séparés ? */
745783
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Une solution équivalente serait d'utiliser un cache L2 multiport, mais l'usage de caches séparés est plus simple.
===L'impact du cache d'instruction sur les performances===
Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions.
: La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale.
D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
===Les avantages et inconvénients des caches d'instructions===
Les arguments précédents justifient que l'on puisse dédier un cache aux instructions. Cependant, ces arguments sont valables à tous les niveaux de la hiérarchie mémoire, y compris au niveau du cache L2 et L3, qui sont eux unifiés. On n'a pas de cache L2 dédié aux instructions ou aux données, mais un cache L2 unique pour les deux. Comment expliquer alors que la spécialisation se fasse spécifiquement au niveau du cache L1 ? La raison est que les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Les caches L1 et L2/L3 ont des usages différents : cache petit mais rapide pour le L1, gros et lent pour le L2/L3. Et ces contraintes sont déterminantes pour décider si tel ou tel niveau de cache est séparé en deux caches spécialisés ou non.
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs disposant d'un cache avaient un cache unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes. N'oublions pas que les concepteurs de processeurs sont limités en transistors et doivent faire des choix. Les transistors utilisés pour le cache d'instruction auraient pu être utilisés pour autre chose, comme augmenter la capacité des caches existants, et notamment le cache L1. Ajouter un cache d'instruction demande de faire des choix, de bien peser le pour et le contre, de bien juger des avantages et inconvénients d'un cache d'instruction.
Le premier compromis à faire est celui entre capacité des caches et performances, plus précisément entre le temps d'accès et la capacité totale du cache L1. Pour faire simple, on a le choix entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. Cependant, cela vient avec un défaut qui réduit la capacité effective. Par exemple, pour un cache d'une capacité de 64 kibioctets, on peut décider de réserver 10 kb aux instructions et le reste aux données, ou encore 40 Kb aux instructions, etc. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 Kb et un cache de données de 32 Kb, impossible d'allouer 40 Kb aux données et 20 aux instructions : le cache de données est trop petit. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre. Et cela explique en grande partie pour seul le cache L1 est séparé en deux : c'est le temps d'accès qui prime pour le cache L1, alors que la capacité effective prime pour les niveaux L2 et au-delà.
===La communication du cache d'instruction avec le séquenceur===
Une autre différence entre instructions et données est la suivante : les instructions sont utilisées par le séquenceur et les données par le chemin de données. Et cela se marie bien avec deux caches séparés, placés à des endroits très différents du processeur. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Il est parfois intégré à l'unité de chargement, par simplicité de conception du processeur. Quant au cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés. Pour simplifier, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une architecture Harvard modifiée. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
Une telle organisation facilite l'implémentation de certaines optimisations. Citons comme exemple, la technique dite du '''prédécodage''', qui accélère le décodage des instructions. Lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction.Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée.
Le prédécodage est surtout utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
===Le cache d'instruction est souvent en lecture seule===
Un point important est que les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===L'usage d'un cache L1 unique demande d'utiliser un cache multiport===
En théorie, on pourrait utiliser un cache L1 unique et le relier à la fois au séquenceur et au chemin de données. Mais utiliser un seul cache unifié demanderait un effort de câblage assez important, le cache devant être à la fois proche du séquenceur et du chemin de données. Les connexions entre le cache L1 unifié et le reste du processeur sont donc assez longues, tortueuses, et difficiles à câbler. De plus, ces longues connexions font que le transfert des bits prend plus de temps pour traverser le fil en longueur, ce qui pose des problèmes à haute fréquence. Avec deux caches séparés, on n'a pas ce problème, ce qui permet de garder des caches L1 très rapides. La lenteur et les problèmes de connexion sont reportés aux connexions entre les caches L1 et le cache L2, mais celui-ci accepte des temps d'accès plus longs.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Mais cet avantage peut s'obtenir avec un cache L1 unique, en utilisant un cache multiport, avec un port relié au séquenceur et un autre au chemin de données. Et le choix entre les deux n'est pas évident. Les caches multiports sont clairement une solution viable : les caches L2 et L3 sont tous des caches multiports. Là encore, tout est histoire de compromis : les mémoires multiport sont plus lentes, plus grosses, plus compliquées à fabriquer. L'impact en termes de temps d'accès est en faveur de la mémoire simple port, tout comme la simplicité de conception. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. Pour les caches L1 de petite capacité, le temps d'accès est très important, ce qui favorise les caches séparés. De plus, utiliser deux caches séparés n'a pas trop d'impact sur le budget en transistors, car les caches L1 sont petits. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que l'économie en circuits est significative.
Et cette histoire de cache simple ou multiport est de plus en plus contraignante. Les processeurs modernes sont capables d’exécuter plusieurs instructions en parallèle, comme on le verra dans quelques chapitres. Et la conséquence est que les caches L1 doivent être capables de lire/écrire plusieurs données en même temps, tout en chargeant plusieurs instructions simultanément. Les deux caches L doivent donc être multiports tous les deux. Le choix est donc entre deux caches avec chacun un nombre limité de ports, ou un cache unique avec beaucoup de ports. S'il fallait utiliser un cache unique, celui-ci aurait au moins une dizaine de ports, voire plus, ce qui serait impraticable. Les concepteurs de processeurs se facilitent la vie en utilisant deux caches séparés avec peu de ports. Mais le fond du compromis est le même : soit un cache rapide avec peu de ports, soit un cache plus lent avec beaucoup de ports.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
lhqawvhkc2t6231hep2u9k31hizp6bb
745784
745783
2025-07-02T18:11:42Z
Mewtow
31375
/* Un exemple de cache : le cache d'instruction */
745784
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Une solution équivalente serait d'utiliser un cache L2 multiport, mais l'usage de caches séparés est plus simple.
===Les avantages et inconvénients des caches d'instructions===
Les arguments précédents justifient que l'on puisse dédier un cache aux instructions. Cependant, ces arguments sont valables à tous les niveaux de la hiérarchie mémoire, y compris au niveau du cache L2 et L3, qui sont eux unifiés. On n'a pas de cache L2 dédié aux instructions ou aux données, mais un cache L2 unique pour les deux. Comment expliquer alors que la spécialisation se fasse spécifiquement au niveau du cache L1 ? La raison est que les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Les caches L1 et L2/L3 ont des usages différents : cache petit mais rapide pour le L1, gros et lent pour le L2/L3. Et ces contraintes sont déterminantes pour décider si tel ou tel niveau de cache est séparé en deux caches spécialisés ou non.
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs disposant d'un cache avaient un cache unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes. N'oublions pas que les concepteurs de processeurs sont limités en transistors et doivent faire des choix. Les transistors utilisés pour le cache d'instruction auraient pu être utilisés pour autre chose, comme augmenter la capacité des caches existants, et notamment le cache L1. Ajouter un cache d'instruction demande de faire des choix, de bien peser le pour et le contre, de bien juger des avantages et inconvénients d'un cache d'instruction.
Le premier compromis à faire est celui entre capacité des caches et performances, plus précisément entre le temps d'accès et la capacité totale du cache L1. Pour faire simple, on a le choix entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. Cependant, cela vient avec un défaut qui réduit la capacité effective. Par exemple, pour un cache d'une capacité de 64 kibioctets, on peut décider de réserver 10 kb aux instructions et le reste aux données, ou encore 40 Kb aux instructions, etc. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 Kb et un cache de données de 32 Kb, impossible d'allouer 40 Kb aux données et 20 aux instructions : le cache de données est trop petit. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre. Et cela explique en grande partie pour seul le cache L1 est séparé en deux : c'est le temps d'accès qui prime pour le cache L1, alors que la capacité effective prime pour les niveaux L2 et au-delà.
===La communication du cache d'instruction avec le séquenceur===
Une autre différence entre instructions et données est la suivante : les instructions sont utilisées par le séquenceur et les données par le chemin de données. Et cela se marie bien avec deux caches séparés, placés à des endroits très différents du processeur. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Il est parfois intégré à l'unité de chargement, par simplicité de conception du processeur. Quant au cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés. Pour simplifier, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une architecture Harvard modifiée. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
Une telle organisation facilite l'implémentation de certaines optimisations. Citons comme exemple, la technique dite du '''prédécodage''', qui accélère le décodage des instructions. Lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction.Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée.
Le prédécodage est surtout utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
===Le cache d'instruction est souvent en lecture seule===
Un point important est que les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===L'usage d'un cache L1 unique demande d'utiliser un cache multiport===
En théorie, on pourrait utiliser un cache L1 unique et le relier à la fois au séquenceur et au chemin de données. Mais utiliser un seul cache unifié demanderait un effort de câblage assez important, le cache devant être à la fois proche du séquenceur et du chemin de données. Les connexions entre le cache L1 unifié et le reste du processeur sont donc assez longues, tortueuses, et difficiles à câbler. De plus, ces longues connexions font que le transfert des bits prend plus de temps pour traverser le fil en longueur, ce qui pose des problèmes à haute fréquence. Avec deux caches séparés, on n'a pas ce problème, ce qui permet de garder des caches L1 très rapides. La lenteur et les problèmes de connexion sont reportés aux connexions entre les caches L1 et le cache L2, mais celui-ci accepte des temps d'accès plus longs.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Mais cet avantage peut s'obtenir avec un cache L1 unique, en utilisant un cache multiport, avec un port relié au séquenceur et un autre au chemin de données. Et le choix entre les deux n'est pas évident. Les caches multiports sont clairement une solution viable : les caches L2 et L3 sont tous des caches multiports. Là encore, tout est histoire de compromis : les mémoires multiport sont plus lentes, plus grosses, plus compliquées à fabriquer. L'impact en termes de temps d'accès est en faveur de la mémoire simple port, tout comme la simplicité de conception. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. Pour les caches L1 de petite capacité, le temps d'accès est très important, ce qui favorise les caches séparés. De plus, utiliser deux caches séparés n'a pas trop d'impact sur le budget en transistors, car les caches L1 sont petits. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que l'économie en circuits est significative.
Et cette histoire de cache simple ou multiport est de plus en plus contraignante. Les processeurs modernes sont capables d’exécuter plusieurs instructions en parallèle, comme on le verra dans quelques chapitres. Et la conséquence est que les caches L1 doivent être capables de lire/écrire plusieurs données en même temps, tout en chargeant plusieurs instructions simultanément. Les deux caches L doivent donc être multiports tous les deux. Le choix est donc entre deux caches avec chacun un nombre limité de ports, ou un cache unique avec beaucoup de ports. S'il fallait utiliser un cache unique, celui-ci aurait au moins une dizaine de ports, voire plus, ce qui serait impraticable. Les concepteurs de processeurs se facilitent la vie en utilisant deux caches séparés avec peu de ports. Mais le fond du compromis est le même : soit un cache rapide avec peu de ports, soit un cache plus lent avec beaucoup de ports.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
===L'impact du cache d'instruction sur les performances===
Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions.
: La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale.
D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
a9pa7amr6v82bufc3s5s382lmmo5fmh
745785
745784
2025-07-02T18:12:15Z
Mewtow
31375
/* Un exemple de cache : le cache d'instruction */
745785
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Une solution équivalente serait d'utiliser un cache L2 multiport, mais l'usage de caches séparés est plus simple.
===Les avantages et inconvénients des caches d'instructions===
Vous vous demandez sans doute pourquoi les caches L2 et L3 sont unifiés. On n'a pas de cache L2 dédié aux instructions ou aux données, mais un cache L2 unique pour les deux. Comment expliquer alors que la spécialisation se fasse spécifiquement au niveau du cache L1 ? La raison est que les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Les caches L1 et L2/L3 ont des usages différents : cache petit mais rapide pour le L1, gros et lent pour le L2/L3. Et ces contraintes sont déterminantes pour décider si tel ou tel niveau de cache est séparé en deux caches spécialisés ou non.
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs disposant d'un cache avaient un cache unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes. N'oublions pas que les concepteurs de processeurs sont limités en transistors et doivent faire des choix. Les transistors utilisés pour le cache d'instruction auraient pu être utilisés pour autre chose, comme augmenter la capacité des caches existants, et notamment le cache L1. Ajouter un cache d'instruction demande de faire des choix, de bien peser le pour et le contre, de bien juger des avantages et inconvénients d'un cache d'instruction.
Le premier compromis à faire est celui entre capacité des caches et performances, plus précisément entre le temps d'accès et la capacité totale du cache L1. Pour faire simple, on a le choix entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. Cependant, cela vient avec un défaut qui réduit la capacité effective. Par exemple, pour un cache d'une capacité de 64 kibioctets, on peut décider de réserver 10 kb aux instructions et le reste aux données, ou encore 40 Kb aux instructions, etc. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 Kb et un cache de données de 32 Kb, impossible d'allouer 40 Kb aux données et 20 aux instructions : le cache de données est trop petit. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre. Et cela explique en grande partie pour seul le cache L1 est séparé en deux : c'est le temps d'accès qui prime pour le cache L1, alors que la capacité effective prime pour les niveaux L2 et au-delà.
===La communication du cache d'instruction avec le séquenceur===
Une autre différence entre instructions et données est la suivante : les instructions sont utilisées par le séquenceur et les données par le chemin de données. Et cela se marie bien avec deux caches séparés, placés à des endroits très différents du processeur. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Il est parfois intégré à l'unité de chargement, par simplicité de conception du processeur. Quant au cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés. Pour simplifier, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une architecture Harvard modifiée. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
Une telle organisation facilite l'implémentation de certaines optimisations. Citons comme exemple, la technique dite du '''prédécodage''', qui accélère le décodage des instructions. Lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction.Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée.
Le prédécodage est surtout utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
===Le cache d'instruction est souvent en lecture seule===
Un point important est que les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===L'usage d'un cache L1 unique demande d'utiliser un cache multiport===
En théorie, on pourrait utiliser un cache L1 unique et le relier à la fois au séquenceur et au chemin de données. Mais utiliser un seul cache unifié demanderait un effort de câblage assez important, le cache devant être à la fois proche du séquenceur et du chemin de données. Les connexions entre le cache L1 unifié et le reste du processeur sont donc assez longues, tortueuses, et difficiles à câbler. De plus, ces longues connexions font que le transfert des bits prend plus de temps pour traverser le fil en longueur, ce qui pose des problèmes à haute fréquence. Avec deux caches séparés, on n'a pas ce problème, ce qui permet de garder des caches L1 très rapides. La lenteur et les problèmes de connexion sont reportés aux connexions entre les caches L1 et le cache L2, mais celui-ci accepte des temps d'accès plus longs.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Mais cet avantage peut s'obtenir avec un cache L1 unique, en utilisant un cache multiport, avec un port relié au séquenceur et un autre au chemin de données. Et le choix entre les deux n'est pas évident. Les caches multiports sont clairement une solution viable : les caches L2 et L3 sont tous des caches multiports. Là encore, tout est histoire de compromis : les mémoires multiport sont plus lentes, plus grosses, plus compliquées à fabriquer. L'impact en termes de temps d'accès est en faveur de la mémoire simple port, tout comme la simplicité de conception. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. Pour les caches L1 de petite capacité, le temps d'accès est très important, ce qui favorise les caches séparés. De plus, utiliser deux caches séparés n'a pas trop d'impact sur le budget en transistors, car les caches L1 sont petits. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que l'économie en circuits est significative.
Et cette histoire de cache simple ou multiport est de plus en plus contraignante. Les processeurs modernes sont capables d’exécuter plusieurs instructions en parallèle, comme on le verra dans quelques chapitres. Et la conséquence est que les caches L1 doivent être capables de lire/écrire plusieurs données en même temps, tout en chargeant plusieurs instructions simultanément. Les deux caches L doivent donc être multiports tous les deux. Le choix est donc entre deux caches avec chacun un nombre limité de ports, ou un cache unique avec beaucoup de ports. S'il fallait utiliser un cache unique, celui-ci aurait au moins une dizaine de ports, voire plus, ce qui serait impraticable. Les concepteurs de processeurs se facilitent la vie en utilisant deux caches séparés avec peu de ports. Mais le fond du compromis est le même : soit un cache rapide avec peu de ports, soit un cache plus lent avec beaucoup de ports.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
===L'impact du cache d'instruction sur les performances===
Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions.
: La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale.
D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
g6giimr0lphk7ocuacw7o8k4c8qnvig
745786
745785
2025-07-02T18:13:13Z
Mewtow
31375
/* Un exemple de cache : le cache d'instruction */
745786
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Une solution équivalente serait d'utiliser un cache L1 unique multiport, mais l'usage de caches séparés est plus simple.
Vous vous demandez sans doute pourquoi les caches L2 et L3 sont unifiés. On n'a pas de cache L2 dédié aux instructions ou aux données, mais un cache L2 unique pour les deux. Comment expliquer alors que la spécialisation se fasse spécifiquement au niveau du cache L1 ? La raison est que les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Les caches L1 et L2/L3 ont des usages différents : cache petit mais rapide pour le L1, gros et lent pour le L2/L3. Et ces contraintes sont déterminantes pour décider si tel ou tel niveau de cache est séparé en deux caches spécialisés ou non.
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs disposant d'un cache avaient un cache unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes. N'oublions pas que les concepteurs de processeurs sont limités en transistors et doivent faire des choix.
Le premier compromis à faire est celui entre capacité des caches et performances, plus précisément entre le temps d'accès et la capacité totale du cache L1. Pour faire simple, on a le choix entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. Cependant, cela vient avec un défaut qui réduit la capacité effective. Par exemple, pour un cache d'une capacité de 64 kibioctets, on peut décider de réserver 10 kb aux instructions et le reste aux données, ou encore 40 Kb aux instructions, etc. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 Kb et un cache de données de 32 Kb, impossible d'allouer 40 Kb aux données et 20 aux instructions : le cache de données est trop petit. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre. Et cela explique en grande partie pour seul le cache L1 est séparé en deux : c'est le temps d'accès qui prime pour le cache L1, alors que la capacité effective prime pour les niveaux L2 et au-delà.
===La communication du cache d'instruction avec le séquenceur===
Une autre différence entre instructions et données est la suivante : les instructions sont utilisées par le séquenceur et les données par le chemin de données. Et cela se marie bien avec deux caches séparés, placés à des endroits très différents du processeur. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Il est parfois intégré à l'unité de chargement, par simplicité de conception du processeur. Quant au cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés. Pour simplifier, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une architecture Harvard modifiée. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
Une telle organisation facilite l'implémentation de certaines optimisations. Citons comme exemple, la technique dite du '''prédécodage''', qui accélère le décodage des instructions. Lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction.Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée.
Le prédécodage est surtout utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
===Le cache d'instruction est souvent en lecture seule===
Un point important est que les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===L'usage d'un cache L1 unique demande d'utiliser un cache multiport===
En théorie, on pourrait utiliser un cache L1 unique et le relier à la fois au séquenceur et au chemin de données. Mais utiliser un seul cache unifié demanderait un effort de câblage assez important, le cache devant être à la fois proche du séquenceur et du chemin de données. Les connexions entre le cache L1 unifié et le reste du processeur sont donc assez longues, tortueuses, et difficiles à câbler. De plus, ces longues connexions font que le transfert des bits prend plus de temps pour traverser le fil en longueur, ce qui pose des problèmes à haute fréquence. Avec deux caches séparés, on n'a pas ce problème, ce qui permet de garder des caches L1 très rapides. La lenteur et les problèmes de connexion sont reportés aux connexions entre les caches L1 et le cache L2, mais celui-ci accepte des temps d'accès plus longs.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Mais cet avantage peut s'obtenir avec un cache L1 unique, en utilisant un cache multiport, avec un port relié au séquenceur et un autre au chemin de données. Et le choix entre les deux n'est pas évident. Les caches multiports sont clairement une solution viable : les caches L2 et L3 sont tous des caches multiports. Là encore, tout est histoire de compromis : les mémoires multiport sont plus lentes, plus grosses, plus compliquées à fabriquer. L'impact en termes de temps d'accès est en faveur de la mémoire simple port, tout comme la simplicité de conception. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. Pour les caches L1 de petite capacité, le temps d'accès est très important, ce qui favorise les caches séparés. De plus, utiliser deux caches séparés n'a pas trop d'impact sur le budget en transistors, car les caches L1 sont petits. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que l'économie en circuits est significative.
Et cette histoire de cache simple ou multiport est de plus en plus contraignante. Les processeurs modernes sont capables d’exécuter plusieurs instructions en parallèle, comme on le verra dans quelques chapitres. Et la conséquence est que les caches L1 doivent être capables de lire/écrire plusieurs données en même temps, tout en chargeant plusieurs instructions simultanément. Les deux caches L doivent donc être multiports tous les deux. Le choix est donc entre deux caches avec chacun un nombre limité de ports, ou un cache unique avec beaucoup de ports. S'il fallait utiliser un cache unique, celui-ci aurait au moins une dizaine de ports, voire plus, ce qui serait impraticable. Les concepteurs de processeurs se facilitent la vie en utilisant deux caches séparés avec peu de ports. Mais le fond du compromis est le même : soit un cache rapide avec peu de ports, soit un cache plus lent avec beaucoup de ports.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
===L'impact du cache d'instruction sur les performances===
Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions.
: La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale.
D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
abuzp41lp0z4faf172aecxuyhznvvan
745787
745786
2025-07-02T18:18:20Z
Mewtow
31375
/* Un exemple de cache : le cache d'instruction */
745787
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions.
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes.
Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. Cependant, cela vient avec un défaut qui réduit la capacité effective.
Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre.
Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données. La raison est que les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Le temps d'accès qui prime pour le cache L1, alors que la capacité effective prime pour les niveaux L2 et au-delà.
===La communication du cache d'instruction avec le séquenceur===
Une autre différence entre instructions et données est la suivante : les instructions sont utilisées par le séquenceur et les données par le chemin de données. Et cela se marie bien avec deux caches séparés, placés à des endroits très différents du processeur. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Il est parfois intégré à l'unité de chargement, par simplicité de conception du processeur. Quant au cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés. Pour simplifier, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une architecture Harvard modifiée. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
Une telle organisation facilite l'implémentation de certaines optimisations. Citons comme exemple, la technique dite du '''prédécodage''', qui accélère le décodage des instructions. Lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction.Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée.
Le prédécodage est surtout utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
===Le cache d'instruction est souvent en lecture seule===
Un point important est que les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===L'usage d'un cache L1 unique demande d'utiliser un cache multiport===
En théorie, on pourrait utiliser un cache L1 unique et le relier à la fois au séquenceur et au chemin de données. Mais utiliser un seul cache unifié demanderait un effort de câblage assez important, le cache devant être à la fois proche du séquenceur et du chemin de données. Les connexions entre le cache L1 unifié et le reste du processeur sont donc assez longues, tortueuses, et difficiles à câbler. De plus, ces longues connexions font que le transfert des bits prend plus de temps pour traverser le fil en longueur, ce qui pose des problèmes à haute fréquence. Avec deux caches séparés, on n'a pas ce problème, ce qui permet de garder des caches L1 très rapides. La lenteur et les problèmes de connexion sont reportés aux connexions entre les caches L1 et le cache L2, mais celui-ci accepte des temps d'accès plus longs.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Mais cet avantage peut s'obtenir avec un cache L1 unique, en utilisant un cache multiport, avec un port relié au séquenceur et un autre au chemin de données. Et le choix entre les deux n'est pas évident. Les caches multiports sont clairement une solution viable : les caches L2 et L3 sont tous des caches multiports. Là encore, tout est histoire de compromis : les mémoires multiport sont plus lentes, plus grosses, plus compliquées à fabriquer. L'impact en termes de temps d'accès est en faveur de la mémoire simple port, tout comme la simplicité de conception. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. Pour les caches L1 de petite capacité, le temps d'accès est très important, ce qui favorise les caches séparés. De plus, utiliser deux caches séparés n'a pas trop d'impact sur le budget en transistors, car les caches L1 sont petits. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que l'économie en circuits est significative.
Et cette histoire de cache simple ou multiport est de plus en plus contraignante. Les processeurs modernes sont capables d’exécuter plusieurs instructions en parallèle, comme on le verra dans quelques chapitres. Et la conséquence est que les caches L1 doivent être capables de lire/écrire plusieurs données en même temps, tout en chargeant plusieurs instructions simultanément. Les deux caches L doivent donc être multiports tous les deux. Le choix est donc entre deux caches avec chacun un nombre limité de ports, ou un cache unique avec beaucoup de ports. S'il fallait utiliser un cache unique, celui-ci aurait au moins une dizaine de ports, voire plus, ce qui serait impraticable. Les concepteurs de processeurs se facilitent la vie en utilisant deux caches séparés avec peu de ports. Mais le fond du compromis est le même : soit un cache rapide avec peu de ports, soit un cache plus lent avec beaucoup de ports.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
===L'impact du cache d'instruction sur les performances===
Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions.
: La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale.
D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
fmjx5dot68f5h40onczzqh7vsy3vmyc
745788
745787
2025-07-02T18:20:43Z
Mewtow
31375
/* La communication du cache d'instruction avec le séquenceur */
745788
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions.
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes.
Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. Cependant, cela vient avec un défaut qui réduit la capacité effective.
Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre.
Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données. La raison est que les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Le temps d'accès qui prime pour le cache L1, alors que la capacité effective prime pour les niveaux L2 et au-delà.
===La communication du cache d'instruction avec le séquenceur===
Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Le cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une '''architecture Harvard modifiée'''. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
Une telle organisation facilite l'implémentation de certaines optimisations. Citons comme exemple, la technique dite du '''prédécodage''', qui accélère le décodage des instructions. Lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction.Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée.
Le prédécodage est surtout utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
===Le cache d'instruction est souvent en lecture seule===
Un point important est que les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===L'usage d'un cache L1 unique demande d'utiliser un cache multiport===
En théorie, on pourrait utiliser un cache L1 unique et le relier à la fois au séquenceur et au chemin de données. Mais utiliser un seul cache unifié demanderait un effort de câblage assez important, le cache devant être à la fois proche du séquenceur et du chemin de données. Les connexions entre le cache L1 unifié et le reste du processeur sont donc assez longues, tortueuses, et difficiles à câbler. De plus, ces longues connexions font que le transfert des bits prend plus de temps pour traverser le fil en longueur, ce qui pose des problèmes à haute fréquence. Avec deux caches séparés, on n'a pas ce problème, ce qui permet de garder des caches L1 très rapides. La lenteur et les problèmes de connexion sont reportés aux connexions entre les caches L1 et le cache L2, mais celui-ci accepte des temps d'accès plus longs.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Mais cet avantage peut s'obtenir avec un cache L1 unique, en utilisant un cache multiport, avec un port relié au séquenceur et un autre au chemin de données. Et le choix entre les deux n'est pas évident. Les caches multiports sont clairement une solution viable : les caches L2 et L3 sont tous des caches multiports. Là encore, tout est histoire de compromis : les mémoires multiport sont plus lentes, plus grosses, plus compliquées à fabriquer. L'impact en termes de temps d'accès est en faveur de la mémoire simple port, tout comme la simplicité de conception. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. Pour les caches L1 de petite capacité, le temps d'accès est très important, ce qui favorise les caches séparés. De plus, utiliser deux caches séparés n'a pas trop d'impact sur le budget en transistors, car les caches L1 sont petits. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que l'économie en circuits est significative.
Et cette histoire de cache simple ou multiport est de plus en plus contraignante. Les processeurs modernes sont capables d’exécuter plusieurs instructions en parallèle, comme on le verra dans quelques chapitres. Et la conséquence est que les caches L1 doivent être capables de lire/écrire plusieurs données en même temps, tout en chargeant plusieurs instructions simultanément. Les deux caches L doivent donc être multiports tous les deux. Le choix est donc entre deux caches avec chacun un nombre limité de ports, ou un cache unique avec beaucoup de ports. S'il fallait utiliser un cache unique, celui-ci aurait au moins une dizaine de ports, voire plus, ce qui serait impraticable. Les concepteurs de processeurs se facilitent la vie en utilisant deux caches séparés avec peu de ports. Mais le fond du compromis est le même : soit un cache rapide avec peu de ports, soit un cache plus lent avec beaucoup de ports.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
===L'impact du cache d'instruction sur les performances===
Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions.
: La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale.
D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
9g56zva3snmklbuqgxxf046yrlqitr2
745789
745788
2025-07-02T18:21:39Z
Mewtow
31375
/* Un exemple de cache : le cache d'instruction */
745789
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions.
Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Le cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une '''architecture Harvard modifiée'''. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
===Pourquoi scinder le cache L1 en cache d'instruction et de données===
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes.
Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. Cependant, cela vient avec un défaut qui réduit la capacité effective.
Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre.
Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données. La raison est que les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Le temps d'accès qui prime pour le cache L1, alors que la capacité effective prime pour les niveaux L2 et au-delà.
===Le prédécodage d'instructions===
La présence d'un cache d'instruction permet l'implémentation de certaines optimisations. Citons comme exemple, la technique dite du '''prédécodage''', qui accélère le décodage des instructions. Lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction.Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée.
Le prédécodage est surtout utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
===Le cache d'instruction est souvent en lecture seule===
Un point important est que les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===L'usage d'un cache L1 unique demande d'utiliser un cache multiport===
En théorie, on pourrait utiliser un cache L1 unique et le relier à la fois au séquenceur et au chemin de données. Mais utiliser un seul cache unifié demanderait un effort de câblage assez important, le cache devant être à la fois proche du séquenceur et du chemin de données. Les connexions entre le cache L1 unifié et le reste du processeur sont donc assez longues, tortueuses, et difficiles à câbler. De plus, ces longues connexions font que le transfert des bits prend plus de temps pour traverser le fil en longueur, ce qui pose des problèmes à haute fréquence. Avec deux caches séparés, on n'a pas ce problème, ce qui permet de garder des caches L1 très rapides. La lenteur et les problèmes de connexion sont reportés aux connexions entre les caches L1 et le cache L2, mais celui-ci accepte des temps d'accès plus longs.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Mais cet avantage peut s'obtenir avec un cache L1 unique, en utilisant un cache multiport, avec un port relié au séquenceur et un autre au chemin de données. Et le choix entre les deux n'est pas évident. Les caches multiports sont clairement une solution viable : les caches L2 et L3 sont tous des caches multiports. Là encore, tout est histoire de compromis : les mémoires multiport sont plus lentes, plus grosses, plus compliquées à fabriquer. L'impact en termes de temps d'accès est en faveur de la mémoire simple port, tout comme la simplicité de conception. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. Pour les caches L1 de petite capacité, le temps d'accès est très important, ce qui favorise les caches séparés. De plus, utiliser deux caches séparés n'a pas trop d'impact sur le budget en transistors, car les caches L1 sont petits. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que l'économie en circuits est significative.
Et cette histoire de cache simple ou multiport est de plus en plus contraignante. Les processeurs modernes sont capables d’exécuter plusieurs instructions en parallèle, comme on le verra dans quelques chapitres. Et la conséquence est que les caches L1 doivent être capables de lire/écrire plusieurs données en même temps, tout en chargeant plusieurs instructions simultanément. Les deux caches L doivent donc être multiports tous les deux. Le choix est donc entre deux caches avec chacun un nombre limité de ports, ou un cache unique avec beaucoup de ports. S'il fallait utiliser un cache unique, celui-ci aurait au moins une dizaine de ports, voire plus, ce qui serait impraticable. Les concepteurs de processeurs se facilitent la vie en utilisant deux caches séparés avec peu de ports. Mais le fond du compromis est le même : soit un cache rapide avec peu de ports, soit un cache plus lent avec beaucoup de ports.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
===L'impact du cache d'instruction sur les performances===
Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions.
: La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale.
D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
9h8w8isincbk5y5kf6nq34ldxwtl8hl
745790
745789
2025-07-02T18:22:43Z
Mewtow
31375
/* Le cache d'instruction est souvent en lecture seule */
745790
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions.
Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Le cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une '''architecture Harvard modifiée'''. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
===Pourquoi scinder le cache L1 en cache d'instruction et de données===
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes.
Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. Cependant, cela vient avec un défaut qui réduit la capacité effective.
Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre.
Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données. La raison est que les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Le temps d'accès qui prime pour le cache L1, alors que la capacité effective prime pour les niveaux L2 et au-delà.
===Le prédécodage d'instructions===
La présence d'un cache d'instruction permet l'implémentation de certaines optimisations. Citons comme exemple, la technique dite du '''prédécodage''', qui accélère le décodage des instructions. Lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction.Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée.
Le prédécodage est surtout utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
===Le cache d'instruction est souvent en lecture seule===
Les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===L'usage d'un cache L1 unique demande d'utiliser un cache multiport===
En théorie, on pourrait utiliser un cache L1 unique et le relier à la fois au séquenceur et au chemin de données. Mais utiliser un seul cache unifié demanderait un effort de câblage assez important, le cache devant être à la fois proche du séquenceur et du chemin de données. Les connexions entre le cache L1 unifié et le reste du processeur sont donc assez longues, tortueuses, et difficiles à câbler. De plus, ces longues connexions font que le transfert des bits prend plus de temps pour traverser le fil en longueur, ce qui pose des problèmes à haute fréquence. Avec deux caches séparés, on n'a pas ce problème, ce qui permet de garder des caches L1 très rapides. La lenteur et les problèmes de connexion sont reportés aux connexions entre les caches L1 et le cache L2, mais celui-ci accepte des temps d'accès plus longs.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Mais cet avantage peut s'obtenir avec un cache L1 unique, en utilisant un cache multiport, avec un port relié au séquenceur et un autre au chemin de données. Et le choix entre les deux n'est pas évident. Les caches multiports sont clairement une solution viable : les caches L2 et L3 sont tous des caches multiports. Là encore, tout est histoire de compromis : les mémoires multiport sont plus lentes, plus grosses, plus compliquées à fabriquer. L'impact en termes de temps d'accès est en faveur de la mémoire simple port, tout comme la simplicité de conception. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. Pour les caches L1 de petite capacité, le temps d'accès est très important, ce qui favorise les caches séparés. De plus, utiliser deux caches séparés n'a pas trop d'impact sur le budget en transistors, car les caches L1 sont petits. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que l'économie en circuits est significative.
Et cette histoire de cache simple ou multiport est de plus en plus contraignante. Les processeurs modernes sont capables d’exécuter plusieurs instructions en parallèle, comme on le verra dans quelques chapitres. Et la conséquence est que les caches L1 doivent être capables de lire/écrire plusieurs données en même temps, tout en chargeant plusieurs instructions simultanément. Les deux caches L doivent donc être multiports tous les deux. Le choix est donc entre deux caches avec chacun un nombre limité de ports, ou un cache unique avec beaucoup de ports. S'il fallait utiliser un cache unique, celui-ci aurait au moins une dizaine de ports, voire plus, ce qui serait impraticable. Les concepteurs de processeurs se facilitent la vie en utilisant deux caches séparés avec peu de ports. Mais le fond du compromis est le même : soit un cache rapide avec peu de ports, soit un cache plus lent avec beaucoup de ports.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
===L'impact du cache d'instruction sur les performances===
Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions.
: La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale.
D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
hkqvh5bks8a1nxxe9arya727s3pq65d
745791
745790
2025-07-02T18:23:59Z
Mewtow
31375
/* Un exemple de cache : le cache d'instruction */
745791
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions.
Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Le cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une '''architecture Harvard modifiée'''. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
===Le prédécodage d'instructions===
La présence d'un cache d'instruction permet l'implémentation de certaines optimisations. Citons comme exemple, la technique dite du '''prédécodage''', qui accélère le décodage des instructions. Lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction.Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée.
Le prédécodage est surtout utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
===Le cache d'instruction est souvent en lecture seule===
Les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===Pourquoi scinder le cache L1 en cache d'instruction et de données===
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes.
Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. Cependant, cela vient avec un défaut qui réduit la capacité effective.
Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre.
Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données. La raison est que les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Le temps d'accès qui prime pour le cache L1, alors que la capacité effective prime pour les niveaux L2 et au-delà. Mais il y a d'autres raisons.
En théorie, on pourrait utiliser un cache L1 unique et le relier à la fois au séquenceur et au chemin de données. Mais utiliser un seul cache unifié demanderait un effort de câblage assez important, le cache devant être à la fois proche du séquenceur et du chemin de données. Les connexions entre le cache L1 unifié et le reste du processeur seraient longues, tortueuses, et difficiles à câbler. De plus, ces longues connexions auraient un temps de trajet très important, ce qui réduit la fréquence. Avec deux caches séparés, on n'a pas ce problème, ce qui permet de garder des caches L1 très rapides. La lenteur et les problèmes de connexion sont reportés aux connexions entre les caches L1 et le cache L2, mais celui-ci accepte des temps d'accès plus longs.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Mais cet avantage peut s'obtenir avec un cache L1 unique, en utilisant un cache multiport, avec un port relié au séquenceur et un autre au chemin de données. Et le choix entre les deux n'est pas évident. Les caches multiports sont clairement une solution viable : les caches L2 et L3 sont tous des caches multiports. Là encore, tout est histoire de compromis : les mémoires multiport sont plus lentes, plus grosses, plus compliquées à fabriquer. L'impact en termes de temps d'accès est en faveur de la mémoire simple port, tout comme la simplicité de conception. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. Pour les caches L1 de petite capacité, le temps d'accès est très important, ce qui favorise les caches séparés. De plus, utiliser deux caches séparés n'a pas trop d'impact sur le budget en transistors, car les caches L1 sont petits. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que l'économie en circuits est significative.
Et cette histoire de cache simple ou multiport est de plus en plus contraignante. Les processeurs modernes sont capables d’exécuter plusieurs instructions en parallèle, comme on le verra dans quelques chapitres. Et la conséquence est que les caches L1 doivent être capables de lire/écrire plusieurs données en même temps, tout en chargeant plusieurs instructions simultanément. Les deux caches L doivent donc être multiports tous les deux. Le choix est donc entre deux caches avec chacun un nombre limité de ports, ou un cache unique avec beaucoup de ports. S'il fallait utiliser un cache unique, celui-ci aurait au moins une dizaine de ports, voire plus, ce qui serait impraticable. Les concepteurs de processeurs se facilitent la vie en utilisant deux caches séparés avec peu de ports. Mais le fond du compromis est le même : soit un cache rapide avec peu de ports, soit un cache plus lent avec beaucoup de ports.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
===L'impact du cache d'instruction sur les performances===
Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions.
: La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale.
D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
tkpm8soufj5s1jiok2wnqnbpglpo7r5
745792
745791
2025-07-02T18:38:31Z
Mewtow
31375
/* Le prédécodage d'instructions */
745792
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions.
Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Le cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une '''architecture Harvard modifiée'''. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
===Le prédécodage d'instructions===
La présence d'un cache d'instruction permet l'implémentation de certaines optimisations, dont la plus connue est la technique dite du '''prédécodage'''. Avec elle, lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction. Le décodage de l'instruction proprement dit est plus court, car une partie du travail est faite en avance, on gagne quelques cycles.
Le prédécodage est particulièrement utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée. En clair : une partie de la capacité totale du cache d'instruction est utilisée pour les informations de pré-décodage. Le prédécodage est donc un compromis : un cache d'instruction de plus faible capacité, mais un décodage plus simple.
Le pré-décodage est surtout utile pour les instructions qui sont ré-exécutées souvent. Pour les instructions exécutées une seule fois, le gain en performance dépend de l'efficacité du préchargement et d'autres contraintes, mais ce qui est gagné lors du décodage est souvent partiellement perdu lors du prédécodage. Par contre, si une instruction est exécutée plusieurs fois, le pré-décodage est fait une seule fois, alors qu'on a un gain à chaque ré-exécution de l'instruction.
===Le cache d'instruction est souvent en lecture seule===
Les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===Pourquoi scinder le cache L1 en cache d'instruction et de données===
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes.
Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. Cependant, cela vient avec un défaut qui réduit la capacité effective.
Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre.
Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données. La raison est que les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Le temps d'accès qui prime pour le cache L1, alors que la capacité effective prime pour les niveaux L2 et au-delà. Mais il y a d'autres raisons.
En théorie, on pourrait utiliser un cache L1 unique et le relier à la fois au séquenceur et au chemin de données. Mais utiliser un seul cache unifié demanderait un effort de câblage assez important, le cache devant être à la fois proche du séquenceur et du chemin de données. Les connexions entre le cache L1 unifié et le reste du processeur seraient longues, tortueuses, et difficiles à câbler. De plus, ces longues connexions auraient un temps de trajet très important, ce qui réduit la fréquence. Avec deux caches séparés, on n'a pas ce problème, ce qui permet de garder des caches L1 très rapides. La lenteur et les problèmes de connexion sont reportés aux connexions entre les caches L1 et le cache L2, mais celui-ci accepte des temps d'accès plus longs.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Mais cet avantage peut s'obtenir avec un cache L1 unique, en utilisant un cache multiport, avec un port relié au séquenceur et un autre au chemin de données. Et le choix entre les deux n'est pas évident. Les caches multiports sont clairement une solution viable : les caches L2 et L3 sont tous des caches multiports. Là encore, tout est histoire de compromis : les mémoires multiport sont plus lentes, plus grosses, plus compliquées à fabriquer. L'impact en termes de temps d'accès est en faveur de la mémoire simple port, tout comme la simplicité de conception. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. Pour les caches L1 de petite capacité, le temps d'accès est très important, ce qui favorise les caches séparés. De plus, utiliser deux caches séparés n'a pas trop d'impact sur le budget en transistors, car les caches L1 sont petits. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que l'économie en circuits est significative.
Et cette histoire de cache simple ou multiport est de plus en plus contraignante. Les processeurs modernes sont capables d’exécuter plusieurs instructions en parallèle, comme on le verra dans quelques chapitres. Et la conséquence est que les caches L1 doivent être capables de lire/écrire plusieurs données en même temps, tout en chargeant plusieurs instructions simultanément. Les deux caches L doivent donc être multiports tous les deux. Le choix est donc entre deux caches avec chacun un nombre limité de ports, ou un cache unique avec beaucoup de ports. S'il fallait utiliser un cache unique, celui-ci aurait au moins une dizaine de ports, voire plus, ce qui serait impraticable. Les concepteurs de processeurs se facilitent la vie en utilisant deux caches séparés avec peu de ports. Mais le fond du compromis est le même : soit un cache rapide avec peu de ports, soit un cache plus lent avec beaucoup de ports.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
===L'impact du cache d'instruction sur les performances===
Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions.
: La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale.
D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
bu0j1igoadnnqpqxszidramd88vjh3b
745793
745792
2025-07-02T18:41:14Z
Mewtow
31375
/* Pourquoi scinder le cache L1 en cache d'instruction et de données */
745793
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions.
Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Le cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une '''architecture Harvard modifiée'''. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
===Le prédécodage d'instructions===
La présence d'un cache d'instruction permet l'implémentation de certaines optimisations, dont la plus connue est la technique dite du '''prédécodage'''. Avec elle, lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction. Le décodage de l'instruction proprement dit est plus court, car une partie du travail est faite en avance, on gagne quelques cycles.
Le prédécodage est particulièrement utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée. En clair : une partie de la capacité totale du cache d'instruction est utilisée pour les informations de pré-décodage. Le prédécodage est donc un compromis : un cache d'instruction de plus faible capacité, mais un décodage plus simple.
Le pré-décodage est surtout utile pour les instructions qui sont ré-exécutées souvent. Pour les instructions exécutées une seule fois, le gain en performance dépend de l'efficacité du préchargement et d'autres contraintes, mais ce qui est gagné lors du décodage est souvent partiellement perdu lors du prédécodage. Par contre, si une instruction est exécutée plusieurs fois, le pré-décodage est fait une seule fois, alors qu'on a un gain à chaque ré-exécution de l'instruction.
===Le cache d'instruction est souvent en lecture seule===
Les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===Pourquoi scinder le cache L1 en cache d'instruction et de données===
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes.
Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. Cependant, cela vient avec un défaut qui réduit la capacité effective.
Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre.
Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données. La raison est que les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Le temps d'accès qui prime pour le cache L1, alors que la capacité effective prime pour les niveaux L2 et au-delà. Mais il y a d'autres raisons.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Mais cet avantage peut s'obtenir avec un cache L1 unique, en utilisant un cache multiport, avec un port relié au séquenceur et un autre au chemin de données. Et le choix entre les deux n'est pas évident. Les caches multiports sont clairement une solution viable : les caches L2 et L3 sont tous des caches multiports. Là encore, tout est histoire de compromis : les mémoires multiport sont plus lentes, plus grosses, plus compliquées à fabriquer. L'impact en termes de temps d'accès est en faveur de la mémoire simple port, tout comme la simplicité de conception. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. Pour les caches L1 de petite capacité, le temps d'accès est très important, ce qui favorise les caches séparés. De plus, utiliser deux caches séparés n'a pas trop d'impact sur le budget en transistors, car les caches L1 sont petits. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que l'économie en circuits est significative.
Et cette histoire de cache simple ou multiport est de plus en plus contraignante. Les processeurs modernes sont capables d’exécuter plusieurs instructions en parallèle, comme on le verra dans quelques chapitres. Et la conséquence est que les caches L1 doivent être capables de lire/écrire plusieurs données en même temps, tout en chargeant plusieurs instructions simultanément. Les deux caches L doivent donc être multiports tous les deux. Le choix est donc entre deux caches avec chacun un nombre limité de ports, ou un cache unique avec beaucoup de ports. S'il fallait utiliser un cache unique, celui-ci aurait au moins une dizaine de ports, voire plus, ce qui serait impraticable. Les concepteurs de processeurs se facilitent la vie en utilisant deux caches séparés avec peu de ports. Mais le fond du compromis est le même : soit un cache rapide avec peu de ports, soit un cache plus lent avec beaucoup de ports.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
===L'impact du cache d'instruction sur les performances===
Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions.
: La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale.
D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
163cz1gnh6js906d9uilkuwdhsbmnf7
745794
745793
2025-07-02T18:52:36Z
Mewtow
31375
/* Un exemple de cache : le cache d'instruction */
745794
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions.
Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Le cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une '''architecture Harvard modifiée'''. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre
===Le prédécodage d'instructions===
La présence d'un cache d'instruction permet l'implémentation de certaines optimisations, dont la plus connue est la technique dite du '''prédécodage'''. Avec elle, lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction. Le décodage de l'instruction proprement dit est plus court, car une partie du travail est faite en avance, on gagne quelques cycles.
Le prédécodage est particulièrement utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée. En clair : une partie de la capacité totale du cache d'instruction est utilisée pour les informations de pré-décodage. Le prédécodage est donc un compromis : un cache d'instruction de plus faible capacité, mais un décodage plus simple.
Le pré-décodage est surtout utile pour les instructions qui sont ré-exécutées souvent. Pour les instructions exécutées une seule fois, le gain en performance dépend de l'efficacité du préchargement et d'autres contraintes, mais ce qui est gagné lors du décodage est souvent partiellement perdu lors du prédécodage. Par contre, si une instruction est exécutée plusieurs fois, le pré-décodage est fait une seule fois, alors qu'on a un gain à chaque ré-exécution de l'instruction.
===Le cache d'instruction est souvent en lecture seule===
Les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===Pourquoi scinder le cache L1 en cache d'instruction et de données===
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes.
Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. L'impact en termes de temps d'accès est en faveur de la mémoire simple port, tout comme la simplicité de conception. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches.
Utiliser deux caches séparés vient avec un autre défaut qui réduit la capacité effective. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre.
Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données. La raison est que les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Pour les caches L1 de petite capacité, le temps d'accès est très important, ce qui favorise les caches séparés. De plus, utiliser deux caches séparés n'a pas trop d'impact sur le budget en transistors, car les caches L1 sont petits. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que l'économie en circuits est significative. De plus, la capacité effective prime pour les niveaux L2 et au-delà, alors qu'elle l'est moins pour le L1.
Et cette histoire de cache simple ou multiport est de plus en plus contraignante. Les processeurs modernes sont capables d’exécuter plusieurs instructions en parallèle, comme on le verra dans quelques chapitres. Et la conséquence est que les caches L1 doivent être capables de lire/écrire plusieurs données en même temps, tout en chargeant plusieurs instructions simultanément. Les deux caches L doivent donc être multiports tous les deux. Le choix est donc entre deux caches avec chacun un nombre limité de ports, ou un cache unique avec beaucoup de ports. S'il fallait utiliser un cache unique, celui-ci aurait au moins une dizaine de ports, voire plus, ce qui serait impraticable. Les concepteurs de processeurs se facilitent la vie en utilisant deux caches séparés avec peu de ports. Mais le fond du compromis est le même : soit un cache rapide avec peu de ports, soit un cache plus lent avec beaucoup de ports.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
===L'impact du cache d'instruction sur les performances===
Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions.
: La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale.
D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
jrumyr7uzwv5l6q1bsh3edkx0lq2ucz
745795
745794
2025-07-02T18:55:12Z
Mewtow
31375
/* Pourquoi scinder le cache L1 en cache d'instruction et de données */
745795
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions.
Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Le cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une '''architecture Harvard modifiée'''. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre
===Le prédécodage d'instructions===
La présence d'un cache d'instruction permet l'implémentation de certaines optimisations, dont la plus connue est la technique dite du '''prédécodage'''. Avec elle, lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction. Le décodage de l'instruction proprement dit est plus court, car une partie du travail est faite en avance, on gagne quelques cycles.
Le prédécodage est particulièrement utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée. En clair : une partie de la capacité totale du cache d'instruction est utilisée pour les informations de pré-décodage. Le prédécodage est donc un compromis : un cache d'instruction de plus faible capacité, mais un décodage plus simple.
Le pré-décodage est surtout utile pour les instructions qui sont ré-exécutées souvent. Pour les instructions exécutées une seule fois, le gain en performance dépend de l'efficacité du préchargement et d'autres contraintes, mais ce qui est gagné lors du décodage est souvent partiellement perdu lors du prédécodage. Par contre, si une instruction est exécutée plusieurs fois, le pré-décodage est fait une seule fois, alors qu'on a un gain à chaque ré-exécution de l'instruction.
===Le cache d'instruction est souvent en lecture seule===
Les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===Pourquoi scinder le cache L1 en cache d'instruction et de données===
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes.
Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. En termes de temps d'accès, la meilleure solution est celle des deux caches simple port. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. La différence est surtout notable pour les gros caches, moins pour les petits caches.
Il faut aussi tenir compte de la capacité effective. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre.
Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données : les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Pour les caches L1, le temps d'accès est plus important que la capacité, ce qui favorise les caches séparés. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que la capacité effective et l'économie en circuits sont significatives.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
===L'impact du cache d'instruction sur les performances===
Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions.
: La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale.
D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
ah5nqra1lm7yhiu7q3aeuk4j075m0dh
745796
745795
2025-07-02T18:56:00Z
Mewtow
31375
/* Un exemple de cache : le cache d'instruction */
745796
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. A la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), qui sont une variante des caches ''direct-mapped'', dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Il est possible d'appliquer la même méthode sur un cache associatif par voie, mais il y a des méthodes plus simples, qui permettent là aussi d’accéder au cache en plusieurs étapes consécutives.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Un exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. A chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace.
De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. On parlait alors de '''''Cache on a stick''''' (COAST). Un exemple est celui des processeurs Pentium 2, qui avaient un cache L2 de ce type. On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner sans mémoire cache, avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
A l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO.
Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, vus dans ce chapitre dans la section sur les caches splittés. Les tags étaient intégrés dans le circuit intégré du processeur, mais les données étaient mémorisées dans une puce d'eDRAM séparée. La puce eDRAM correspondait en réalité à une DRAM adressable qui servait de DRAM pour les données et mémorisaient les voies du cache.
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions.
Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Le cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une '''architecture Harvard modifiée'''. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre
===Pourquoi scinder le cache L1 en cache d'instruction et de données===
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache L1 multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes.
Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. En termes de temps d'accès, la meilleure solution est celle des deux caches simple port. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. La différence est surtout notable pour les gros caches, moins pour les petits caches.
Il faut aussi tenir compte de la capacité effective. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre.
Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données : les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Pour les caches L1, le temps d'accès est plus important que la capacité, ce qui favorise les caches séparés. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que la capacité effective et l'économie en circuits sont significatives.
===Le prédécodage d'instructions===
La présence d'un cache d'instruction permet l'implémentation de certaines optimisations, dont la plus connue est la technique dite du '''prédécodage'''. Avec elle, lorsque les instructions sont chargées dans le cache d'instruction, elles sont partiellement décodées, grâce à un circuit séparé de l'unité de décodage d'instruction. Le décodage de l'instruction proprement dit est plus court, car une partie du travail est faite en avance, on gagne quelques cycles.
Le prédécodage est particulièrement utile avec des instructions de taille variable : il permet de pré-déterminer où commencent/terminent les instructions dans une ligne de cache, indiquer leur taille, etc. Autre possibilité, le prédécodage peut indiquer s'il y a des branchements dans une ligne de cache et où ils se trouvent, ce qui est très utile pour la prédiction de branchement.
[[File:Prédécodage des instructions dans le cache L1.png|centre|vignette|upright=2.5|Prédécodage des instructions dans le cache L1]]
Pour chaque ligne de cache, le décodage partiel fournit des informations utiles au décodeur d'instruction. Les informations pré-décodées sont soit intégrée dans la ligne de cache, soit mémorisées dans une banque séparée. En clair : une partie de la capacité totale du cache d'instruction est utilisée pour les informations de pré-décodage. Le prédécodage est donc un compromis : un cache d'instruction de plus faible capacité, mais un décodage plus simple.
Le pré-décodage est surtout utile pour les instructions qui sont ré-exécutées souvent. Pour les instructions exécutées une seule fois, le gain en performance dépend de l'efficacité du préchargement et d'autres contraintes, mais ce qui est gagné lors du décodage est souvent partiellement perdu lors du prédécodage. Par contre, si une instruction est exécutée plusieurs fois, le pré-décodage est fait une seule fois, alors qu'on a un gain à chaque ré-exécution de l'instruction.
===Le cache d'instruction est souvent en lecture seule===
Les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
===L'impact du cache d'instruction sur les performances===
Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions.
: La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale.
D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. A l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les technologies RAID
| prevText=Les technologies RAID
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
nut086pmtl7vmbhl3sar6tfjut3547m
Fonctionnement d'un ordinateur/Les circuits de calcul flottant
0
68985
745807
739461
2025-07-02T19:38:04Z
Mewtow
31375
745807
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Un point important est que les circuits de calcul flottants effectuent des calculs, mais aussi des tâches de normalisation et d'arrondis. La '''normalisation''' corrige le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
Avant le calcul, il y a aussi une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle a une seconde fonction : corriger les deux opérandes pour qu'elles soient additionnables. En effet, on peut additionner deux flottants très simplement si leurs deux exposants sont égaux. D'où une étape pour mettre les deux opérandes au même exposant, en modifiant leur mantisse, avant de faire le calcul.
Il faut noter que la normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait que l'additionneur-soustracteur utilisé est un additionneur-soustracteur spécifiques à cette représentation.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
===La racine carrée flottante===
Le calcul de la racine carrée d'un flottant est relativement simple. Par définition, la racine carrée d'un flottant <math>m \times 2^e</math> vaut :
: <math>\sqrt{m \times 2^e}</math>
La racine d'un produit est le produit des racines :
: <math>\sqrt{m} \times \sqrt{2^e}</math>
Vu que <math>\sqrt{x} = x^{\frac{1}{2}}</math>, on a :
: <math>\sqrt{m} \times 2^{\frac{e}{2}}</math>
On voit qu'il suffit de calculer la racine carrée de la mantisse et de diviser l'exposant par deux (ou le décaler d'un rang vers la droite ce qui est équivalent). Voici le circuit que cela doit donner :
[[File:Racine carrée FPU.PNG|centre|vignette|upright=2|Racine carrée FPU]]
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais ce n'est pas le cas pour la plupart des calculs flottants qu'on souhaite faire, ce qui n’empêche cependant pas de ruser. L'idée est de mettre les deux flottants au même exposant, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Crcuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==La normalisation et les arrondis flottants==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, mais il doit aussi passer par d'autres étapes dites de normalisation.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
f4975wbj5buhs9fvydj4g8rjt60weaf
745808
745807
2025-07-02T19:38:34Z
Mewtow
31375
745808
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Un point important est que les circuits de calcul flottants effectuent des calculs, mais aussi des tâches de normalisation et d'arrondis. La '''normalisation''' corrige le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
Avant le calcul, il y a aussi une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle a une seconde fonction : corriger les deux opérandes pour qu'elles soient additionnables. En effet, on peut additionner deux flottants très simplement si leurs deux exposants sont égaux. D'où une étape pour mettre les deux opérandes au même exposant, en modifiant leur mantisse, avant de faire le calcul.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait que l'additionneur-soustracteur utilisé est un additionneur-soustracteur spécifiques à cette représentation.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
===La racine carrée flottante===
Le calcul de la racine carrée d'un flottant est relativement simple. Par définition, la racine carrée d'un flottant <math>m \times 2^e</math> vaut :
: <math>\sqrt{m \times 2^e}</math>
La racine d'un produit est le produit des racines :
: <math>\sqrt{m} \times \sqrt{2^e}</math>
Vu que <math>\sqrt{x} = x^{\frac{1}{2}}</math>, on a :
: <math>\sqrt{m} \times 2^{\frac{e}{2}}</math>
On voit qu'il suffit de calculer la racine carrée de la mantisse et de diviser l'exposant par deux (ou le décaler d'un rang vers la droite ce qui est équivalent). Voici le circuit que cela doit donner :
[[File:Racine carrée FPU.PNG|centre|vignette|upright=2|Racine carrée FPU]]
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais ce n'est pas le cas pour la plupart des calculs flottants qu'on souhaite faire, ce qui n’empêche cependant pas de ruser. L'idée est de mettre les deux flottants au même exposant, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Crcuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==La normalisation et les arrondis flottants==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, mais il doit aussi passer par d'autres étapes dites de normalisation.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
lh1yay1il4jv1apx8o5mh44jfugn5vk
745809
745808
2025-07-02T19:39:59Z
Mewtow
31375
/* L'addition et la soustraction flottante */
745809
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Un point important est que les circuits de calcul flottants effectuent des calculs, mais aussi des tâches de normalisation et d'arrondis. La '''normalisation''' corrige le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
Avant le calcul, il y a aussi une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle a une seconde fonction : corriger les deux opérandes pour qu'elles soient additionnables. En effet, on peut additionner deux flottants très simplement si leurs deux exposants sont égaux. D'où une étape pour mettre les deux opérandes au même exposant, en modifiant leur mantisse, avant de faire le calcul.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait que l'additionneur-soustracteur utilisé est un additionneur-soustracteur spécifiques à cette représentation.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
===La racine carrée flottante===
Le calcul de la racine carrée d'un flottant est relativement simple. Par définition, la racine carrée d'un flottant <math>m \times 2^e</math> vaut :
: <math>\sqrt{m \times 2^e}</math>
La racine d'un produit est le produit des racines :
: <math>\sqrt{m} \times \sqrt{2^e}</math>
Vu que <math>\sqrt{x} = x^{\frac{1}{2}}</math>, on a :
: <math>\sqrt{m} \times 2^{\frac{e}{2}}</math>
On voit qu'il suffit de calculer la racine carrée de la mantisse et de diviser l'exposant par deux (ou le décaler d'un rang vers la droite ce qui est équivalent). Voici le circuit que cela doit donner :
[[File:Racine carrée FPU.PNG|centre|vignette|upright=2|Racine carrée FPU]]
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Crcuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==La normalisation et les arrondis flottants==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, mais il doit aussi passer par d'autres étapes dites de normalisation.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
fhbjapl5pwkerinlt5hhkiv4kkr5e8y
745810
745809
2025-07-02T19:42:01Z
Mewtow
31375
745810
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Un point important est que les circuits de calcul flottants effectuent des calculs, mais aussi des tâches de normalisation et d'arrondis. La '''normalisation''' corrige le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
Avant le calcul, il y a aussi une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle a une seconde fonction : corriger les deux opérandes pour qu'elles soient additionnables. En effet, on peut additionner deux flottants très simplement si leurs deux exposants sont égaux. D'où une étape pour mettre les deux opérandes au même exposant, en modifiant leur mantisse, avant de faire le calcul.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Pour simplifier, les processeur modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Il s'agit de circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs.
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait que l'additionneur-soustracteur utilisé est un additionneur-soustracteur spécifiques à cette représentation.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
===La racine carrée flottante===
Le calcul de la racine carrée d'un flottant est relativement simple. Par définition, la racine carrée d'un flottant <math>m \times 2^e</math> vaut :
: <math>\sqrt{m \times 2^e}</math>
La racine d'un produit est le produit des racines :
: <math>\sqrt{m} \times \sqrt{2^e}</math>
Vu que <math>\sqrt{x} = x^{\frac{1}{2}}</math>, on a :
: <math>\sqrt{m} \times 2^{\frac{e}{2}}</math>
On voit qu'il suffit de calculer la racine carrée de la mantisse et de diviser l'exposant par deux (ou le décaler d'un rang vers la droite ce qui est équivalent). Voici le circuit que cela doit donner :
[[File:Racine carrée FPU.PNG|centre|vignette|upright=2|Racine carrée FPU]]
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Crcuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==La normalisation et les arrondis flottants==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, mais il doit aussi passer par d'autres étapes dites de normalisation.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
r0hxj5tr8zgvzhq8osheklm3oa59485
745811
745810
2025-07-02T19:42:12Z
Mewtow
31375
/* La racine carrée flottante */
745811
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Un point important est que les circuits de calcul flottants effectuent des calculs, mais aussi des tâches de normalisation et d'arrondis. La '''normalisation''' corrige le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
Avant le calcul, il y a aussi une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle a une seconde fonction : corriger les deux opérandes pour qu'elles soient additionnables. En effet, on peut additionner deux flottants très simplement si leurs deux exposants sont égaux. D'où une étape pour mettre les deux opérandes au même exposant, en modifiant leur mantisse, avant de faire le calcul.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Pour simplifier, les processeur modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Il s'agit de circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs.
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait que l'additionneur-soustracteur utilisé est un additionneur-soustracteur spécifiques à cette représentation.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Crcuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==La normalisation et les arrondis flottants==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, mais il doit aussi passer par d'autres étapes dites de normalisation.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
ap4zezox1r0065zmm9w8xl1y5hvifte
745812
745811
2025-07-02T19:43:51Z
Mewtow
31375
/* La multiplication flottante */
745812
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Un point important est que les circuits de calcul flottants effectuent des calculs, mais aussi des tâches de normalisation et d'arrondis. La '''normalisation''' corrige le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
Avant le calcul, il y a aussi une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle a une seconde fonction : corriger les deux opérandes pour qu'elles soient additionnables. En effet, on peut additionner deux flottants très simplement si leurs deux exposants sont égaux. D'où une étape pour mettre les deux opérandes au même exposant, en modifiant leur mantisse, avant de faire le calcul.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Pour simplifier, les processeur modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Il s'agit de circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs.
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Crcuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==La normalisation et les arrondis flottants==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, mais il doit aussi passer par d'autres étapes dites de normalisation.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
mhyhpvpd68cwrxugky595b2wbf6q5w3
745813
745812
2025-07-02T19:46:26Z
Mewtow
31375
745813
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Un point important est que les circuits de calcul flottants effectuent des calculs, mais aussi des tâches de normalisation et d'arrondis. La '''normalisation''' corrige le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
Avant le calcul, il y a aussi une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle a une seconde fonction : corriger les deux opérandes pour qu'elles soient additionnables. En effet, on peut additionner deux flottants très simplement si leurs deux exposants sont égaux. D'où une étape pour mettre les deux opérandes au même exposant, en modifiant leur mantisse, avant de faire le calcul.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Crcuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==La normalisation et les arrondis flottants==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, mais il doit aussi passer par d'autres étapes dites de normalisation.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
g7hdljyio51qy11g3a6zlis3hlmfhhi
745815
745813
2025-07-02T19:51:31Z
Mewtow
31375
745815
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
[[File:Unité de calcul flottante, intérieur.png|centre|vignette|upright=2|Unité de calcul flottante, intérieur]]
==La pré-normalisation et les arrondis==
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Un point important est que les circuits de calcul flottants effectuent des calculs, mais aussi des tâches de normalisation et d'arrondis. La '''normalisation''' corrige le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
Avant le calcul, il y a aussi une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle a une seconde fonction : corriger les deux opérandes pour qu'elles soient additionnables. En effet, on peut additionner deux flottants très simplement si leurs deux exposants sont égaux. D'où une étape pour mettre les deux opérandes au même exposant, en modifiant leur mantisse, avant de faire le calcul.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Circuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==La normalisation et les arrondis flottants==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, mais il doit aussi passer par d'autres étapes dites de normalisation.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
0xbkf8m8tn701m1xc3a9qn2umtkrm87
745817
745815
2025-07-02T19:52:12Z
Mewtow
31375
745817
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
==La pré-normalisation et les arrondis==
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Un point important est que les circuits de calcul flottants effectuent des calculs, mais aussi des tâches de normalisation et d'arrondis. La '''normalisation''' corrige le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
Avant le calcul, il y a aussi une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle a une seconde fonction : corriger les deux opérandes pour qu'elles soient additionnables. En effet, on peut additionner deux flottants très simplement si leurs deux exposants sont égaux. D'où une étape pour mettre les deux opérandes au même exposant, en modifiant leur mantisse, avant de faire le calcul.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Circuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==La normalisation et les arrondis flottants==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, mais il doit aussi passer par d'autres étapes dites de normalisation.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
75mt56psfpus5u4v6xbf31grqs3sq68
745818
745817
2025-07-02T19:52:25Z
Mewtow
31375
/* La pré-normalisation et les arrondis */
745818
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
==La pré-normalisation et les arrondis==
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Un point important est que les circuits de calcul flottants effectuent des calculs, mais aussi des tâches de normalisation et d'arrondis.
La '''normalisation''' corrige le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
Avant le calcul, il y a aussi une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle a une seconde fonction : corriger les deux opérandes pour qu'elles soient additionnables. En effet, on peut additionner deux flottants très simplement si leurs deux exposants sont égaux. D'où une étape pour mettre les deux opérandes au même exposant, en modifiant leur mantisse, avant de faire le calcul.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Circuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==La normalisation et les arrondis flottants==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, mais il doit aussi passer par d'autres étapes dites de normalisation.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
9ay6dhhfd5op9a050h5m7vaa6t4la2a
745819
745818
2025-07-02T19:52:52Z
Mewtow
31375
/* La normalisation et les arrondis flottants */
745819
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
==La pré-normalisation et les arrondis==
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Un point important est que les circuits de calcul flottants effectuent des calculs, mais aussi des tâches de normalisation et d'arrondis.
La '''normalisation''' corrige le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
Avant le calcul, il y a aussi une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle a une seconde fonction : corriger les deux opérandes pour qu'elles soient additionnables. En effet, on peut additionner deux flottants très simplement si leurs deux exposants sont égaux. D'où une étape pour mettre les deux opérandes au même exposant, en modifiant leur mantisse, avant de faire le calcul.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Circuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
8dt31l46yugeclzqelnlcsrctuumv84
745820
745819
2025-07-02T19:53:09Z
Mewtow
31375
/* La pré-normalisation et les arrondis */
745820
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
==La pré-normalisation et les arrondis==
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, mais il doit aussi passer par d'autres étapes dites de normalisation.
La '''normalisation''' corrige le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
Avant le calcul, il y a aussi une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle a une seconde fonction : corriger les deux opérandes pour qu'elles soient additionnables. En effet, on peut additionner deux flottants très simplement si leurs deux exposants sont égaux. D'où une étape pour mettre les deux opérandes au même exposant, en modifiant leur mantisse, avant de faire le calcul.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Circuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
th9oa7o5lnwnwfiojsbllkajxcli9a1
745821
745820
2025-07-02T19:53:20Z
Mewtow
31375
/* La pré-normalisation et les arrondis */
745821
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
==La pré-normalisation et les arrondis==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, mais il doit aussi passer par d'autres étapes dites de normalisation.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
La '''normalisation''' corrige le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
Avant le calcul, il y a aussi une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle a une seconde fonction : corriger les deux opérandes pour qu'elles soient additionnables. En effet, on peut additionner deux flottants très simplement si leurs deux exposants sont égaux. D'où une étape pour mettre les deux opérandes au même exposant, en modifiant leur mantisse, avant de faire le calcul.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Circuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
k06lpniz0grtqr29n7t5u176ahwgrw6
745822
745821
2025-07-02T19:54:29Z
Mewtow
31375
/* La pré-normalisation et les arrondis */
745822
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
==La pré-normalisation et les arrondis==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, mais il doit aussi passer par d'autres étapes dites de normalisation.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Elles corrigent le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
===La pré-normalisation===
Avant le calcul, il y a aussi une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle a une seconde fonction : corriger les deux opérandes pour qu'elles soient additionnables. En effet, on peut additionner deux flottants très simplement si leurs deux exposants sont égaux. D'où une étape pour mettre les deux opérandes au même exposant, en modifiant leur mantisse, avant de faire le calcul.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Circuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
bpusw3jhjtcgpqpdp2uto3u8fhj90ju
745823
745822
2025-07-02T19:55:11Z
Mewtow
31375
/* La pré-normalisation et les arrondis */
745823
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
==La pré-normalisation et les arrondis==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, et doit auissi passer par d'autres étapes dites de normalisation.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Elles corrigent le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
===La pré-normalisation===
Avant le calcul, il y a aussi une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle a une seconde fonction : corriger les deux opérandes pour qu'elles soient additionnables. En effet, on peut additionner deux flottants très simplement si leurs deux exposants sont égaux. D'où une étape pour mettre les deux opérandes au même exposant, en modifiant leur mantisse, avant de faire le calcul.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Circuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
sblie02dgu925jgslve3mblub1jstod
745824
745823
2025-07-02T19:55:51Z
Mewtow
31375
745824
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Si on omet les circuits de normalisation/arrondis dont on parlera plus bas, ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
==La pré-normalisation et les arrondis==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, et doit auissi passer par d'autres étapes dites de normalisation.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Elles corrigent le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
===La pré-normalisation===
Avant le calcul, il y a aussi une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle a une seconde fonction : corriger les deux opérandes pour qu'elles soient additionnables. En effet, on peut additionner deux flottants très simplement si leurs deux exposants sont égaux. D'où une étape pour mettre les deux opérandes au même exposant, en modifiant leur mantisse, avant de faire le calcul.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Circuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
a8og4n9669r7550ua7eijbpcjfl7lae
745825
745824
2025-07-02T20:02:55Z
Mewtow
31375
/* La pré-normalisation et les arrondis */
745825
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Si on omet les circuits de normalisation/arrondis dont on parlera plus bas, ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
==La normalisation et les arrondis==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, et doit auissi passer par d'autres étapes dites de normalisation.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Elles corrigent le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Circuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
ph58apohu72hohf7f4gpxfv9scl5vgi
745826
745825
2025-07-02T20:03:06Z
Mewtow
31375
/* Les multiplications/divisions flottantes */
745826
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Si on omet les circuits de normalisation/arrondis dont on parlera plus bas, ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
==La normalisation et les arrondis==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, et doit auissi passer par d'autres étapes dites de normalisation.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Elles corrigent le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
Avant le calcul, il y a une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle a une seconde fonction : corriger les deux opérandes pour qu'elles soient additionnables. En effet, on peut additionner deux flottants très simplement si leurs deux exposants sont égaux. D'où une étape pour mettre les deux opérandes au même exposant, en modifiant leur mantisse, avant de faire le calcul.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Circuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
2jsos0yxedq6cmp5ykmqk2t50eh455m
745827
745826
2025-07-02T20:03:30Z
Mewtow
31375
745827
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Si on omet les circuits de normalisation/arrondis dont on parlera plus bas, ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
Avant le calcul, il y a une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle a une seconde fonction : corriger les deux opérandes pour qu'elles soient additionnables. En effet, on peut additionner deux flottants très simplement si leurs deux exposants sont égaux. D'où une étape pour mettre les deux opérandes au même exposant, en modifiant leur mantisse, avant de faire le calcul.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Circuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==La normalisation et les arrondis==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, et doit auissi passer par d'autres étapes dites de normalisation.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Elles corrigent le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
q5sb5d9kac563ukn9edkooscdwket6j
745828
745827
2025-07-02T20:04:00Z
Mewtow
31375
/* Les multiplications/divisions flottantes */
745828
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Si on omet les circuits de normalisation/arrondis dont on parlera plus bas, ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
Avant le calcul, il y a une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux). Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle est plus complexe, comme on le verra plus tard.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Circuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==La normalisation et les arrondis==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, et doit auissi passer par d'autres étapes dites de normalisation.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Elles corrigent le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
f1xfn69xnky9i3odtnreg820r9phdgh
745829
745828
2025-07-02T20:05:28Z
Mewtow
31375
/* Les multiplications/divisions flottantes */
745829
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Si on omet les circuits de normalisation/arrondis dont on parlera plus bas, ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
Avant le calcul, il y a une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux), puis l'ajoute aux mantisses. Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle est plus complexe, comme on le verra plus tard.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
===La division flottante===
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Circuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==La normalisation et les arrondis==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, et doit auissi passer par d'autres étapes dites de normalisation.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Elles corrigent le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
q38nj2olytsc1hsvx47a0erdaoqpkqp
745830
745829
2025-07-02T20:31:08Z
Mewtow
31375
/* La division flottante */
745830
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Si on omet les circuits de normalisation/arrondis dont on parlera plus bas, ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
Avant le calcul, il y a une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux), puis l'ajoute aux mantisses. Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle est plus complexe, comme on le verra plus tard.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
===La multiplication entière réalisée par la FPU===
Les processeurs modernes incorporent de nombreux circuits de calcul, dont plusieurs ALU entières, un circuit multiplieur entier, et une unité de calcul flottante. Ils ont donc un multiplieur flottant et un multiplieur entier. Vous remarquerez qu'un multiplieur flottant contient un multiplieur entier pour multiplier les mantisses. Et vous avez sans doute pensé qu'il est possible n'utiliser qu'un seul multiplieur entier pour faire à la fois les multiplications entières et flottantes.
Cette optimisation a été utilisée sur plusieurs processeurs commerciaux. Par exemple, les processeur Atom d'Intel utilisaient cette optimisation : les multiplications entières étaient réalisées dans un multiplier entier partagé avec la FPU. Il en est de même sur les processeurs Athlon d'AMD, sortis dans les années 2000.
Mais pour que cela fonctionne, il faut que le multiplieur entier soit assez long. Par exemple, prenons un processeur 32 bits, c'est à dire qu'il gère des nombres entiers codés sur 32 bits. Il faut que le multiplieur soit un multiplier 32 bits, qui multiplie deux opérandes 32 bits. Pour un processeur 64 bits, il faut que le multiplieur entier fasse 64 bits. Maintenant, regardons quelle taille a le multiplieur pour des opérandes flottantes.
* Avec des flottants simples précisions codés sur 32 bits, les mantisses font 23 bits, ce qui donne des multiplieurs de 24 bits. Ce n'est pas assez pour un processeur 32 ou 64 bits.
* Avec des flottants double précision de 64 bits, les mantisses font 52 bits, ce qui fait un multiplieur de 53 bits. C'est suffisant pour un processeur 32 bits, pas assez sur un processeur 64 bits.
En clair, les processeurs 32 bits pouvaient utiliser cette technique, pas les processeurs 64 bits. Avec cependant une petite nuance. Sur les anciens processeurs x86 des PC, les flottants faisaient 80 bits, avec une mantisse de 64 bits, ce qui est assez à la fois pour les processeurs 32 et 64 bits. Malheureusement, les processeurs 64 bits avaient un bduget en transistor suffisant pour ne pas appliquer cette optimisation. Pour des raisons diverses, il était préférable d'avoir un multiplieur entier séparé du multiplieur flottant.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Circuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==La normalisation et les arrondis==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, et doit auissi passer par d'autres étapes dites de normalisation.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Elles corrigent le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
pns6cqzedlfafaxj7y312ry9m5ounel
745831
745830
2025-07-02T20:33:11Z
Mewtow
31375
/* La multiplication entière réalisée par la FPU */
745831
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Si on omet les circuits de normalisation/arrondis dont on parlera plus bas, ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
Avant le calcul, il y a une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux), puis l'ajoute aux mantisses. Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle est plus complexe, comme on le verra plus tard.
===La multiplication flottante===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
===La multiplication entière réalisée par la FPU===
Vous remarquerez qu'un multiplieur flottant contient un multiplieur entier pour multiplier les mantisses. Et vous avez sans doute pensé qu'il est possible n'utiliser qu'un seul multiplieur entier pour faire à la fois les multiplications entières et flottantes. Cette optimisation a été utilisée sur plusieurs processeurs commerciaux. Par exemple, les processeur Atom d'Intel utilisaient cette optimisation : les multiplications entières étaient réalisées dans un multiplieur entier partagé avec la FPU. Il en est de même sur les processeurs Athlon d'AMD, sortis dans les années 2000.
Mais pour que cela fonctionne, il faut que le multiplieur entier soit assez long. Par exemple, prenons un processeur 32 bits, c'est à dire qu'il gère des nombres entiers codés sur 32 bits. Il faut que le multiplieur soit un multiplier 32 bits, qui multiplie deux opérandes 32 bits. Pour un processeur 64 bits, il faut que le multiplieur entier fasse 64 bits. Maintenant, regardons quelle taille a le multiplieur pour des opérandes flottantes.
* Avec des flottants simples précisions codés sur 32 bits, les mantisses font 23 bits, ce qui donne des multiplieurs de 24 bits. Ce n'est pas assez pour un processeur 32 ou 64 bits.
* Avec des flottants double précision de 64 bits, les mantisses font 52 bits, ce qui fait un multiplieur de 53 bits. C'est suffisant pour un processeur 32 bits, pas assez sur un processeur 64 bits.
En clair, les processeurs 32 bits pouvaient utiliser cette technique, pas les processeurs 64 bits. Avec cependant une petite nuance. Sur les anciens processeurs x86 des PC, les flottants faisaient 80 bits, avec une mantisse de 64 bits, ce qui est assez à la fois pour les processeurs 32 et 64 bits. Malheureusement, les processeurs 64 bits avaient un budget en transistor suffisant pour ne pas appliquer cette optimisation. Pour des raisons diverses, il était préférable d'avoir un multiplieur entier séparé du multiplieur flottant.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Circuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==La normalisation et les arrondis==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, et doit auissi passer par d'autres étapes dites de normalisation.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Elles corrigent le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
acszqyar8ricwzuy5ptto2dyczwu74n
745832
745831
2025-07-02T20:35:04Z
Mewtow
31375
/* La multiplication flottante */
745832
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Si on omet les circuits de normalisation/arrondis dont on parlera plus bas, ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
Avant le calcul, il y a une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux), puis l'ajoute aux mantisses. Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle est plus complexe, comme on le verra plus tard.
===Les circuits multiplieurs/diviseurs flottants===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
===La multiplication entière réalisée par la FPU===
Vous remarquerez qu'un multiplieur flottant contient un multiplieur entier pour multiplier les mantisses. Et vous avez sans doute pensé qu'il est possible n'utiliser qu'un seul multiplieur entier pour faire à la fois les multiplications entières et flottantes. Cette optimisation a été utilisée sur plusieurs processeurs commerciaux. Par exemple, les processeur Atom d'Intel utilisaient cette optimisation : les multiplications entières étaient réalisées dans un multiplieur entier partagé avec la FPU. Il en est de même sur les processeurs Athlon d'AMD, sortis dans les années 2000.
Mais pour que cela fonctionne, il faut que le multiplieur entier soit assez long. Par exemple, prenons un processeur 32 bits, c'est à dire qu'il gère des nombres entiers codés sur 32 bits. Il faut que le multiplieur soit un multiplier 32 bits, qui multiplie deux opérandes 32 bits. Pour un processeur 64 bits, il faut que le multiplieur entier fasse 64 bits. Maintenant, regardons quelle taille a le multiplieur pour des opérandes flottantes.
* Avec des flottants simples précisions codés sur 32 bits, les mantisses font 23 bits, ce qui donne des multiplieurs de 24 bits. Ce n'est pas assez pour un processeur 32 ou 64 bits.
* Avec des flottants double précision de 64 bits, les mantisses font 52 bits, ce qui fait un multiplieur de 53 bits. C'est suffisant pour un processeur 32 bits, pas assez sur un processeur 64 bits.
En clair, les processeurs 32 bits pouvaient utiliser cette technique, pas les processeurs 64 bits. Avec cependant une petite nuance. Sur les anciens processeurs x86 des PC, les flottants faisaient 80 bits, avec une mantisse de 64 bits, ce qui est assez à la fois pour les processeurs 32 et 64 bits. Malheureusement, les processeurs 64 bits avaient un budget en transistor suffisant pour ne pas appliquer cette optimisation. Pour des raisons diverses, il était préférable d'avoir un multiplieur entier séparé du multiplieur flottant.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Circuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==La normalisation et les arrondis==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, et doit auissi passer par d'autres étapes dites de normalisation.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Elles corrigent le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
4n01uouoyx0paihvfbuuqfehhveelqb
745835
745832
2025-07-02T20:42:26Z
Mewtow
31375
/* La multiplication entière réalisée par la FPU */
745835
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Si on omet les circuits de normalisation/arrondis dont on parlera plus bas, ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
Avant le calcul, il y a une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux), puis l'ajoute aux mantisses. Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle est plus complexe, comme on le verra plus tard.
===Les circuits multiplieurs/diviseurs flottants===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
===La multiplication entière réalisée par la FPU===
Vous remarquerez qu'un multiplieur flottant contient un multiplieur entier pour multiplier les mantisses. Et vous avez sans doute pensé qu'il est possible n'utiliser qu'un seul multiplieur entier pour faire à la fois les multiplications entières et flottantes. Cette optimisation a été utilisée sur plusieurs processeurs commerciaux. Par exemple, les processeurs Atom d'Intel utilisaient cette optimisation : les multiplications entières étaient réalisées dans un multiplieur entier partagé avec la FPU. Il en est de même sur les processeurs Athlon d'AMD, sortis dans les années 2000.
Mais pour que cela fonctionne, il faut que le multiplieur entier soit assez long. Par exemple, prenons un processeur 32 bits, c’est-à-dire qu'il gère des nombres entiers codés sur 32 bits. Il faut que le multiplieur soit un multiplieur 32 bits, qui multiplie deux opérandes 32 bits. Pour un processeur 64 bits, il faut que le multiplieur entier fasse 64 bits. Maintenant, regardons quelle taille a le multiplieur pour des opérandes flottants.
* Avec des flottants simples précisions codés sur 32 bits, les mantisses font 23 bits, ce qui donne des multiplieurs de 24 bits. Ce n'est pas assez pour un processeur 32 ou 64 bits.
* Avec des flottants double précision de 64 bits, les mantisses font 52 bits, ce qui fait un multiplieur de 53 bits. C'est suffisant pour un processeur 32 bits, pas assez sur un processeur 64 bits.
En clair, les processeurs 32 bits pouvaient utiliser cette technique, pas les processeurs 64 bits. Avec cependant une petite nuance. Sur les anciens processeurs x86 des PC, les flottants faisaient 80 bits, avec une mantisse de 64 bits, ce qui est assez à la fois pour les processeurs 32 et 64 bits. Malheureusement, les processeurs 64 bits avaient un budget en transistor suffisant pour ne pas appliquer cette optimisation. Pour des raisons diverses, il était préférable d'avoir un multiplieur entier séparé du multiplieur flottant.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Circuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==La normalisation et les arrondis==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, et doit auissi passer par d'autres étapes dites de normalisation.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Elles corrigent le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
8z8q4h9d5cv3hbm3j1cja6d9tlnramn
745842
745835
2025-07-02T20:53:41Z
Mewtow
31375
/* La multiplication entière réalisée par la FPU */
745842
wikitext
text/x-wiki
Dans le chapitre précédent, nous avons vu les circuits de calcul pour les nombres entiers. Il est maintenant temps de voir les circuits pour faire des calculs, mais avec des nombres flottants. Nous allons nous concentrer sur les nombres flottants au format IEEE754, avant de faire un aparté sur les flottants logarithmiques.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Maintenant que cela est dit, voyons comment les processeurs modernes gèrent les calculs flottants. Il est souvent dit qu'un processeur incorpore une unité de calcul spécialisée dans les calculs flottants, appelée la ''Floating Point Unit'', ce qui se traduirait en '''unité de calcul flottante'''. Dans la réalité, les processeurs modernes incorporent plusieurs circuits distincts : un pour multiplier deux flottants, un autre pour additionner deux flottants, et éventuellement un troisième pour la division flottante. Si on omet les circuits de normalisation/arrondis dont on parlera plus bas, ils ne partagent pas de sous-circuits, ce qui fait qu'ils sont implémentés avec des circuits séparés, comme le sont les ALU et les circuits multiplieurs/diviseurs. Leurs sorties sont regroupées à un même multiplexeur, pas plus.
==Les multiplications/divisions flottantes==
Paradoxalement, les multiplications, divisions et racines carrées sont relativement simples à calculer avec des nombres flottants, là où l'addition et la soustraction sont plus complexes. Aussi, nous allons d'abord parler des opérations de multiplications et divisions, avant de poursuivre avec les addition et soustraction, en enfin de terminer avec les procédés de normalisation, arrondis et prénormalisation.
Avant le calcul, il y a une étape de '''prénormalisation''', qui gère le bit implicite des mantisses. Elle détermine si ce bit vaut 0 (flottants dénormaux) ou 1 (les flottants normaux), puis l'ajoute aux mantisses. Pour la multiplication et la division, l'étape de prénormalisation ne fait pas autre chose. Mais pour l'addition et la soustraction, elle est plus complexe, comme on le verra plus tard.
===Les circuits multiplieurs/diviseurs flottants===
Prenons deux nombres flottants de mantisses <math>m_1</math> et <math>m_2</math> et les exposants <math>e_1</math> et <math>e_2</math>. Leur multiplication donne :
: <math>(m_1 \times 2^{e_1}) \times (m_2 \times 2^{e_2})</math>
On regroupe les termes :
: <math>(m_1 \times m_2) \times (2^{e_1} \times 2^{e_2})</math>
On simplifie la puissance :
: <math>(m_1 \times m_2) \times 2^{e_1 + e_2}</math>
En clair, multiplier deux flottants revient à multiplier les mantisses et additionner les exposants. Le circuit est donc composé d'un additionneur-soustracteur et un multiplieur.
Il faut cependant penser à plusieurs choses pas forcément évidentes.
* Premièrement, il faut ajouter les bits implicites aux mantisses avant de les multiplier, ce qui est le rôle de l'étape de pré-normalisation.
* Deuxièmement, il faut se rappeler que les exposants sont encodés en représentation par excès, ce qui fait qu'il faut utiliser un additionneur-soustracteur en représentation par excès.
* Troisièmement, il faut calculer le bit de signe du résultat à partir de ceux des opérandes.
* Enfin, il ne faut pas oublier de rajouter les étapes de normalisation et d'arrondis.
[[File:Multiplieur flottant avec normalisation.PNG|centre|vignette|upright=2|Multiplieur flottant avec normalisation]]
La division fonctionne sur le même principe que la multiplication, si ce n'est que les calculs sont quelque peu différents : les exposants sont soustraits et que les mantisses sont divisées.
Pour le démontrer, prenons deux flottants <math>m_1 \times 2^{e_1}</math> et <math>m_2 \times 2^{e_2}</math> et divisons le premier par le second. On a alors :
: <math>\frac{m1 \times 2^{e_1}}{m2 \times 2^{e_2}}</math>
On applique les règles sur les fractions :
: <math>\frac{m_1}{m_2} \times \frac{2^{e_1}}{2^{e_2}}</math>
On simplifie la puissance de 2 :
: <math>\frac{m_1}{m_2} \times 2^{e_1-e_2}</math>
On voit que les mantisses sont divisées entre elles, tandis que les exposants sont soustraits.
===La multiplication entière réalisée par l'unité de calcul flottante===
Vous remarquerez qu'un multiplieur flottant contient un multiplieur entier pour multiplier les mantisses. Et vous avez sans doute pensé qu'il est possible n'utiliser qu'un seul multiplieur entier pour faire à la fois les multiplications entières et flottantes. Cette optimisation a été utilisée sur plusieurs processeurs commerciaux. Par exemple, les processeurs Atom d'Intel utilisaient cette optimisation : les multiplications entières étaient réalisées dans un multiplieur entier partagé avec l'unité de calcul flottante. Il en est de même sur les processeurs Athlon d'AMD, sortis dans les années 2000.
Mais pour que cela fonctionne, il faut que le multiplieur entier soit assez long. Par exemple, prenons un processeur 32 bits, c’est-à-dire qu'il gère des nombres entiers codés sur 32 bits. Il faut que le multiplieur soit un multiplieur 32 bits, qui multiplie deux opérandes 32 bits. Pour un processeur 64 bits, il faut que le multiplieur entier fasse 64 bits. Maintenant, regardons quelle taille a le multiplieur pour des opérandes flottants.
* Avec des flottants simples précisions codés sur 32 bits, les mantisses font 23 bits, ce qui donne des multiplieurs de 24 bits. Ce n'est pas assez pour un processeur 32 ou 64 bits.
* Avec des flottants double précision de 64 bits, les mantisses font 52 bits, ce qui fait un multiplieur de 53 bits. C'est suffisant pour un processeur 32 bits, pas assez sur un processeur 64 bits.
En clair, les processeurs 32 bits pouvaient utiliser cette technique, pas les processeurs 64 bits. Avec cependant une petite nuance. Sur les anciens processeurs x86 des PC, les flottants faisaient 80 bits, avec une mantisse de 64 bits, ce qui est assez à la fois pour les processeurs 32 et 64 bits. Malheureusement, les processeurs 64 bits avaient un budget en transistor suffisant pour ne pas appliquer cette optimisation. Pour des raisons diverses, il était préférable d'avoir un multiplieur entier séparé du multiplieur flottant.
==L'addition et la soustraction flottante==
La somme de deux flottants se calcule simplement si les exposants des deux opérandes sont égaux : il suffit alors d'additionner les mantisses. Mais que faire si les deux exposants sont différents ? L'astuce est de mettre les deux flottants au même exposant sans en changer leur valeur, de les mettre à l'échelle. L'exposant choisi étant souvent le plus grand exposant des deux flottants. Une fois mises à l'échelle, les deux opérandes sont additionnées, et le résultat est normalisé pour donner un flottant.
Suivant les signes, il faudra additionner ou soustraire les opérandes : additionner une opérande positive avec une négative demande en réalité de faire une soustraction, de même que soustraire une opérande négative demande en réalité de l'additionner. Il faut donc ajouter, avant l'additionneur, un circuit qui détermine s'il faut faire une addition ou une soustraction, en fonction du bit de signe des opérandes, et de s'il faut faire une addition ou une soustraction (opcode de l'opération voulue).
[[File:Crcuit d'addition et de soustraction flottante.jpg|centre|vignette|upright=2|Circuit d'addition et de soustraction flottante.]]
===Le circuit de pré-normalisation===
La mise des deux opérandes au même exposant s'appelle la '''pré-normalisation'''. L'exposant final est choisit parmi les deux opérandes : on prend le plus grand exposant parmi des deux. L'opérande avec le plus grand exposant reste inchangée, elle est conservée telle quelle. Par contre, il faut pré-normaliser l'autre opérande, celui avec le plus petit exposant. Et pour cela, rien de plus simple : il suffit de décaler la mantisse vers la droite, d'un nombre de rangs égal à la différence entre les deux exposants.
Pour faire ce décalage, on utilise un décaleur et un circuit qui échange les deux opérandes. Le circuit d'échange a pour but d'envoyer le plus petit exposant dans le décaleur et est composé de quelques multiplexeurs. Il est piloté par un comparateur qui détermine quel est le nombre avec le plus petit exposant. Nous verrons comment fabriquer un tel comparateur dans le chapitre suivant sur les comparateurs.
[[File:Circuit de mise au même exposant.jpg|centre|vignette|upright=2|Circuit de mise au même exposant.]]
Précisons que le comparateur et le soustracteur peuvent être fusionnés, car un comparateur est en réalité un soustracteur amélioré. Une manière alternative est la suivante. En premier lieu, on soustrait les exposants pour déterminer de combien décaler la mantisse. Le résultat de la soustraction est ensuite envoyé à un circuit qui vérifie si le résultat est positif ou négatif, en vérifiant le bit de poids fort du résultat. Si le résultat est positif, la première opérande est plus grande que la seconde, c'est la seconde opérande qu'il faut pré-normaliser. Si le résultat est négatif, c'est la première opérande qu'il faut prénormaliser.
[[File:Circuit de prénormalisation d'un additionneur flottant.jpg|centre|vignette|upright=2|Circuit de prénormalisation d'un additionneur flottant]]
==La normalisation et les arrondis==
Calculer sur des nombres flottants peut sembler trivial, mais les mathématiques ne sont pas vraiment d'accord avec cela. En effet, le résultat d'un calcul avec des flottants n'est pas forcément un flottant valide. Il doit subir quelques transformations pour être un nombre flottant : il doit souvent être arrondi, et doit auissi passer par d'autres étapes dites de normalisation.
[[File:Normalisation in circuit.png|vignette|upright=1|Normalisation in circuit]]
Elles corrigent le résultat du calcul pour qu'il rentre dans un nombre flottant. Par exemple, si on multiplie deux flottants de 32 bits, l'exposant et la mantisse du résultat sont calculés séparément et les concaténer ne donne pas forcément un nombre flottant 32 bits. Diverses techniques de normalisation et d'arrondis permettent de corriger l'exposant et la mantisse pour donner un flottant 32 bit correct. Et elles auront leur section dédiée.
La normalisation et les arrondis sont gérés différemment suivant le format de flottant utilisé. Les flottants les plus courants suivent la norme IEEE754, où normalisation et arrondis sont standardisés. Mais d'autres formats de flottants exotiques peuvent suivre des règles différentes.
===La normalisation===
La '''normalisation''' gère le bit implicite. Le résultat en sortie d'un circuit de calcul n'a pas forcément son bit implicite à 1. Prenons l'exemple suivant, où on soustrait deux flottants qui ont des mantisses codées sur 8 bits - le format de flottant n'est donc par standard. On soustrait les deux mantisses suivantes, le chiffre entre parenthèse est le bit implicite : (1) 1100 1100 - (1) 1000 1000 = (0) 0100 0100.
Le résultat a un bit implicite à 0, ce qui donne un résultat dénormal. Mais il est parfois possible de convertir ce résultat en un flottant normal, à condition de corriger l'exposant. L'idée est, pour le cas précédent, de décaler la mantisse de deux rangs : (0) 0100 0100 devient (1) 0001 00''00''. Mais décaler la mantisse déforme le résultat : le résultat décalé de deux rangs vers la gauche multiplie le résultat par 4. Mais on peut compenser exactement le tout en corrigeant l'exposant, afin de diviser le résultat final par 4 : il suffit de soustraire deux à l'exposant !
Le cas général est assez similaire, sauf que l'on doit décaler la mantisse par un nombre de rang adéquat, pas forcément 2, et soustraire ce nombre de rangs à l'exposant. Pour savoir de combien de rangs il faut décaler, il faut compter le nombre de zéros situés de poids fort, avec un circuit spécialisé qu'on a vu il y a quelques chapitres, le circuit de CLZ (''Count Leading Zero''). Ce circuit permet aussi de détecter si la mantisse vaut zéro.
[[File:Circuit de prénormalisation.jpg|centre|vignette|upright=2|Circuit de normalisation.]]
===Les arrondis===
Une fois ce résultat calculé, il faut faire un arrondi du résultat avec un circuit d''''arrondi'''. L'arrondi se base sur les bits de poids faible situés juste à gauche et à droite de la virgule., ce qui demande d'analyser une dizaine de bits tout au plus. Une fois les bits de poids faible à gauche de la virgule sont remplacé, les bits à droite sont éliminés. L'arrondi peut être réalisé par un circuit combinatoire, mais le faible nombre de bits d'entrée rend possible d'utiliser une mémoire ROM. Ce qui est réalisé dans quelques unités flottantes.
[[File:Circuit d'arrondi flottant basé sur une ROM.png|centre|vignette|upright=1.5|Circuit d'arrondi flottant basé sur une ROM.]]
Malheureusement, il arrive que ces arrondis décalent la position du bit implicite d'un rang, ce qui se résout avec un décalage si cela arrive. Le circuit de normalisation contient donc de quoi détecter ces débordements et un décaleur. Bien évidemment, l'exposant doit alors lui aussi être corrigé en cas de décalage de la mantisse.
[[File:Circuit de postnormalisation.jpg|centre|vignette|upright=2|Circuit de postnormalisation.]]
===Le circuit de normalisation/arrondi final===
Le circuit complet, qui effectue à la fois normalisation et arrondis est le suivant :
[[File:Circuit de normalisation-arrondi.PNG|centre|vignette|upright=2|Circuit de normalisation-arrondi]]
==Les flottants logarithmiques==
Maintenant, nous allons fabriquer une unité de calcul pour les flottants logarithmiques. Nous avions vu les flottants logarithmiques dans le chapitre [[Fonctionnement d'un ordinateur/Le_codage_des_nombres#Les_nombres_flottants_logarithmiques|Le codage des nombres, dans la section sur les flottants logarithmiques]]. Pour résumer rapidement, ce sont des flottants qui codent uniquement un bit de signe et un exposant, mais sans la mantisse (qui vaut implicitement 1). L'exposant stocké n'est autre que le logarithme en base 2 du nombre codé, d'où le nom donné à ces flottants. Au passage, l'exposant est stocké dans une représentation à virgule fixe.
Nous avions dit dans le chapitre sur le codage des nombres que l'utilité de cette représentation est de simplifier certains calculs, comme les multiplications, divisions, puissances, etc. Eh bien, vous allez rapidement comprendre pourquoi dans cette section. Nous allons commencer par voir les deux opérations de base : la multiplication et la division. Celles-ci sont en effet extrêmement simples dans cet encodage, bien plus que l'addition et la soustraction. C'est d'ailleurs la raison d'être de cet encodage : simplifier fortement les calculs multiplicatifs, quitte à perdre en performance sur les additions/soustractions.
===La multiplication et la division de deux flottants logarithmiques===
Pour commencer, il faut se souvenir d'un théorème de mathématique sur les logarithmes : le logarithme d'un produit est égal à la somme des logarithmes. Dans ces conditions, une multiplication entre deux flottants logarithmiques se transforme en une simple addition d'exposants.
: <math>\log (A \times B) = \log A + \log B</math>
Le même raisonnement peut être tenu pour la division. Dans les calculs précédents, il suffit de se rappeler que diviser par <math>B</math>, c'est multiplier par <math>1 \over B</math>. Or, il faut se rappeler que <math> \log \frac{1}{B} = - \log B </math>. On obtient alors, en combinant ces deux expressions :
: <math>\log \frac{A}{B} = \log A - \log B</math>
La division s'est transformée en simple soustraction. Dans ces conditions, une unité de calcul logarithmique devant effectuer des multiplications et des divisions est constituée d'un simple additionneur/soustracteur et de quelques (ou plusieurs, ça marche aussi) circuits pour corriger le tout.
===L'addition et la soustraction de deux flottants logarithmiques===
Pour l'addition et la soustraction, la situation est beaucoup plus corsée, vu qu'il n'y a pas vraiment de formule mathématique pour simplifier le logarithme d'une somme. Dans ces conditions, la seule solution est d'utiliser une mémoire de précalcul, comme vu au début du chapitre. Et encore une fois, il est possible de réduire la taille de mémoire ROM de précalcul en utilisant des identités mathématiques. L'idée est de transformer l'addition en une opération plus simple, qui peut se pré-calculer plus facilement.
Pour cela, partons de la formule suivante, qui pose l'équivalence des termes suivants :
: <math>\log_2(x+y) = \log_2 \left(x + x \times \frac{y}{x}\right) = \log_2 \left[ x \times \left(1+\frac{y}{x}\right) \right]</math>
Vu que le logarithme d'un produit est égal à la somme des logarithmes, on a :
: <math>\log_2(x+y) = \log_2 x + \log_2 \left(1+\frac{y}{x}\right)</math>
Pour rappel, les représentations de x et y en flottant logarithmique sont égales à <math>\log_2(x)</math> et <math>\log_2(y)</math>. En notant ces dernières <math>e_y</math> et <math>e_x</math>, on a :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{y}{x}\right)</math>
Par définition, <math>y = 2^{e_y}</math> et <math>x = 2^{e_x}</math>. En injectant dans l'équation précédente, on obtient :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+\frac{2^{e_y}}{2^{e_x}}\right)</math>
On simplifie la puissance de deux :
: <math>\log_2(x+y) = e_x + \log_2 \left(1+2^{e_y-e_x}\right)</math>
On a donc :
: <math>\log_2(x+y) = e_x + f(e_y-e_x)</math>, avec f la fonction adéquate.
Pour la soustraction, on a la même chose, sauf que les signes changent, ce qui donne :
: <math>\log_2(x - y) = e_x - g(e_y-e_x)</math>, avec g une fonction différente de f.
On vient donc de trouver la formule qui permet de faire le calcul, le seul obstacle étant la fonction f et la fonction g. Heureusement, le terme de droite peut se pré-calculer facilement, ce qui donne une table beaucoup plus petite qu'avec l'idée initiale. Dans ces conditions, l'addition se traduit en :
* un circuit qui additionne/soustrait les deux opérandes ;
* une table qui prend le résultat de l'additionneur/soustracteur et fournit le terme de droite ;
* et un autre additionneur pour le résultat.
===Résumé===
Pour implémenter les quatre opérations, on a donc besoin :
* de deux additionneurs/soustracteur et d'un diviseur pour l'addition/soustraction ;
* de deux autres additionneurs/soustracteur pour la multiplication et la division ;
* et d'une ROM.
Il est bon de noter qu'il est tout à fait possible de mutualiser les additionneurs pour la multiplication et l'addition. En rajoutant quelques multiplexeurs, on peut faire en sorte que le circuit puisse se configurer pour que les additionneurs servent soit pour la multiplication, soit pour l'addition. On économise en peu de circuits.
[[File:Unité de calcul logarithmique.PNG|centre|vignette|upright=2|Unité de calcul logarithmique]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les circuits pour la multiplication et la division
| prevText=Les circuits pour la multiplication et la division
| next=Les circuits de calcul trigonométriques
| nextText=Les circuits de calcul trigonométriques
}}
</noinclude>
l5vcgdi9p24ybzoobyriq22oxcopzoh
Fonctionnement d'un ordinateur/Le chemin de données
0
69025
745814
745342
2025-07-02T19:47:19Z
Mewtow
31375
/* Les unités de calcul spécialisées */
745814
wikitext
text/x-wiki
Comme vu précédemment, le '''chemin de donnée''' est l'ensemble des composants dans lesquels circulent les données dans le processeur. Il comprend l'unité de calcul, les registres, l'unité de communication avec la mémoire, et le ou les interconnexions qui permettent à tout ce petit monde de communiquer. Dans ce chapitre, nous allons voir ces composants en détail.
==Les unités de calcul==
Le processeur contient des circuits capables de faire des calculs arithmétiques, des opérations logiques, et des comparaisons, qui sont regroupés dans une unité de calcul appelée '''unité arithmétique et logique'''. Certains préfèrent l’appellation anglaise ''arithmetic and logic unit'', ou ALU. Par défaut, ce terme est réservé aux unités de calcul qui manipulent des nombres entiers. Les unités de calcul spécialisées pour les calculs flottants sont désignées par le terme "unité de calcul flottant", ou encore FPU (''Floating Point Unit'').
L'interface d'une unité de calcul est assez simple : on a des entrées pour les opérandes et une sortie pour le résultat du calcul. De plus, les instructions de comparaisons ou de calcul peuvent mettre à jour le registre d'état, qui est relié à une autre sortie de l’unité de calcul. Une autre entrée, l''''entrée de sélection de l'instruction''', spécifie l'opération à effectuer. Elle sert à configurer l'unité de calcul pour faire une addition et pas une multiplication, par exemple. Sur cette entrée, on envoie un numéro qui précise l'opération à effectuer. La correspondance entre ce numéro et l'opération à exécuter dépend de l'unité de calcul. Sur les processeurs où l'encodage des instructions est "simple", une partie de l'opcode de l'instruction est envoyé sur cette entrée.
[[File:Unité de calcul usuelle.png|centre|vignette|upright=2|Unité de calcul usuelle.]]
Il faut signaler que les processeurs modernes possèdent plusieurs unités de calcul, toutes reliées aux registres. Cela permet d’exécuter plusieurs calculs en même temps dans des unités de calcul différentes, afin d'augmenter les performances du processeur. Diverses technologies, abordées dans la suite du cours permettent de profiter au mieux de ces unités de calcul : pipeline, exécution dans le désordre, exécution superscalaire, jeux d'instructions VLIW, etc. Mais laissons cela de côté pour le moment.
===L'ALU entière : additions, soustractions, opérations bit à bit===
Un processeur contient plusieurs ALUs spécialisées. La principale, présente sur tous les processeurs, est l''''ALU entière'''. Elle s'occupe uniquement des opérations sur des nombres entiers, les nombres flottants sont gérés par une ALU à part. Elle gère des opérations simples : additions, soustractions, opérations bit à bit, parfois des décalages/rotations. Par contre, elle ne gère pas la multiplication et la division, qui sont prises en charge par un circuit multiplieur/diviseur à part.
L'ALU entière a déjà été vue dans un chapitre antérieur, nommé "Les unités arithmétiques et logiques entières (simples)", qui expliquait comment en concevoir une. Nous avions vu qu'une ALU entière est une sorte de circuit additionneur-soustracteur amélioré, ce qui explique qu'elle gère des opérations entières simples, mais pas la multiplication ni la division. Nous ne reviendrons pas dessus. Cependant, il y a des choses à dire sur leur intégration au processeur.
Une ALU entière gère souvent une opération particulière, qui ne fait rien et recopie simplement une de ses opérandes sur sa sortie. L'opération en question est appelée l''''opération ''Pass through''''', encore appelée opération NOP. Elle est implémentée en utilisant un simple multiplexeur, placé en sortie de l'ALU. Le fait qu'une ALU puisse effectuer une opération ''Pass through'' permet de fortement simplifier le chemin de donnée, d'économiser des multiplexeurs. Mais nous verrons cela sous peu.
[[File:ALU avec opération NOP.png|centre|vignette|upright=2|ALU avec opération NOP.]]
Avant l'invention du microprocesseur, le processeur n'était pas un circuit intégré unique. L'ALU, le séquenceur et les registres étaient dans des puces séparées. Les ALU étaient vendues séparément et manipulaient des opérandes de 4/8 bits, les ALU 4 bits étaient très fréquentes. Si on voulait créer une ALU pour des opérandes plus grandes, il fallait construire l'ALU en combinant plusieurs ALU 4/8 bits. Par exemple, l'ALU des processeurs AMD Am2900 est une ALU de 16 bits composée de plusieurs sous-ALU de 4 bits. Cette technique qui consiste à créer des unités de calcul à partir d'unités de calcul plus élémentaires s'appelle en jargon technique du '''bit slicing'''. Nous en avions parlé dans le chapitre sur les unités de calcul, aussi nous n'en reparlerons pas plus ici.
L'ALU manipule des opérandes codées sur un certain nombre de bits. Par exemple, une ALU peut manipuler des entiers codés sur 8 bits, sur 16 bits, etc. En général, la taille des opérandes de l'ALU est la même que la taille des registres. Un processeur 32 bits, avec des registres de 32 bit, a une ALU de 32 bits. C'est intuitif, et cela rend l'implémentation du processeur bien plus facile. Mais il y a quelques exceptions, où l'ALU manipule des opérandes plus petits que la taille des registres. Par exemple, de nombreux processeurs 16 bits, avec des registres de 16 bits, utilisent une ALU de 8 bits. Un autre exemple assez connu est celui du Motorola 68000, qui était un processeur 32 bits, mais dont l'ALU faisait juste 16 bits. Son successeur, le 68020, avait lui une ALU de 32 bits.
Sur de tels processeurs, les calculs sont fait en plusieurs passes. Par exemple, avec une ALU 8 bit, les opérations sur des opérandes 8 bits se font en un cycle d'horloge, celles sur 16 bits se font en deux cycles, celles en 32 en quatre, etc. Si un programme manipule assez peu d'opérandes 16/32/64 bits, la perte de performance est assez faible. Diverses techniques visent à améliorer les performances, mais elles ne font pas de miracles. Par exemple, vu que l'ALU est plus courte, il est possible de la faire fonctionner à plus haute fréquence, pour réduire la perte de performance.
Pour comprendre comme est implémenté ce système de passes, prenons l'exemple du processeur 8 bit Z80. Ses registres entiers étaient des registres de 8 bits, alors que l'ALU était de 4 bits. Les calculs étaient faits en deux phases : une qui traite les 4 bits de poids faible, une autre qui traite les 4 bits de poids fort. Pour cela, les opérandes étaient placées dans des registres de 4 bits en entrée de l'ALU, plusieurs multiplexeurs sélectionnaient les 4 bits adéquats, le résultat était mémorisé dans un registre de résultat de 8 bits, un démultiplexeur plaçait les 4 bits du résultat au bon endroit dans ce registre. L'unité de contrôle s'occupait de la commande des multiplexeurs/démultiplexeurs. Les autres processeurs 8 ou 16 bits utilisent des circuits similaires pour faire leurs calculs en plusieurs fois.
[[File:ALU du Z80.png|centre|vignette|upright=2|ALU du Z80]]
Un exemple extrême est celui des des '''processeurs sériels''' (sous-entendu ''bit-sériels''), qui utilisent une '''ALU sérielle''', qui fait leurs calculs bit par bit, un bit à la fois. S'il a existé des processeurs de 1 bit, comme le Motorola MC14500B, la majeure partie des processeurs sériels étaient des processeurs 4, 8 ou 16 bits. L'avantage de ces ALU est qu'elles utilisent peu de transistors, au détriment des performances par rapport aux processeurs non-sériels. Mais un autre avantage est qu'elles peuvent gérer des opérandes de grande taille, avec plus d'une trentaine de bits, sans trop de problèmes.
===Les circuits multiplieurs et diviseurs===
Les processeurs modernes ont une ALU pour les opérations simples (additions, décalages, opérations logiques), couplée à une ALU pour les multiplications, un circuit multiplieur séparé. Précisons qu'il ne sert pas à grand chose de fusionner le circuit multiplieur avec l'ALU, mieux vaut les garder séparés par simplicité. Les processeurs haute performance disposent systématiquement d'un circuit multiplieur et gèrent la multiplication dans leur jeu d'instruction.
Le cas de la division est plus compliqué. La présence d'un circuit multiplieur est commune, mais les circuits diviseurs sont eux très rares. Leur cout en circuit est globalement le même que pour un circuit multiplieur, mais le gain en performance est plus faible. Le gain en performance pour la multiplication est modéré car il s'agit d'une opération très fréquente, alors qu'il est très faible pour la division car celle-ci est beaucoup moins fréquente.
Pour réduire le cout en circuits, il arrive que l'ALU pour les multiplications gère à la fois la multiplication et la division. Les circuits multiplieurs et diviseurs sont en effet très similaires et partagent beaucoup de points communs. Généralement, la fusion se fait pour les multiplieurs/diviseurs itératifs.
Il existe cependant des circuits qui se passent de multiplieurs, tout en supportant la multiplication dans leur jeu d'instruction. Certains utilisent pour cela du microcode, technique qu'on verra dans deux chapitres, mais l'Intel Atom utilise une technique franchement peu ordinaire. L'Intel Atom utilise l'unité de calcul flottante pour faire les multiplications entières. Les opérandes entières sont traduites en nombres flottants, multipliés par l'unité de calcul flottante, puis le résultat est converti en un entier avec quelques corrections à la clé. Ainsi, on fait des économies de circuits, en mutualisant le multiplieur entre l'unité de calcul flottante et l'ALU entière, surtout que ce multiplieur manipule des opérandes plus courtes. Les performances sont cependant réduites comparé à l'usage d'un vrai multiplieur entier.
===Le ''barrel shifter''===
On vient d'expliquer que la présence de plusieurs ALU spécialisée est très utile pour implémenter des opérations compliquées à insérer dans une unité de calcul normale, comme la multiplication et la division. Mais les décalages sont aussi dans ce cas, de même que les rotations. Nous avions vu il y a quelques chapitres qu'ils sont réalisés par un circuit spécialisé, appelé un ''barrel shifter'', qu'il est difficile de fusionner avec une ALU normale. Aussi, beaucoup de processeurs incorporent un ''barrel shifter'' séparé de l'ALU.
Les processeurs ARM utilise un ''barrel shifter'', mais d'une manière un peu spéciale. On a vu il y a quelques chapitres que si on fait une opération logique, une addition, une soustraction ou une comparaison, la seconde opérande peut être décalée automatiquement. L'instruction incorpore le type de de décalage à faire et par combien de rangs il faut décaler directement à côté de l'opcode. Cela simplifie grandement les calculs d'adresse, qui se font en une seule instruction, contre deux ou trois sur d'autres architectures. Et pour cela, l'ALU proprement dite est précédée par un ''barrel shifter'',une seconde ALU spécialisée dans les décalages. Notons que les instructions MOV font aussi partie des instructions où la seconde opérande (le registre source) peut être décalé : cela signifie que les MOV passent par l'ALU, qui effectue alors un NOP, une opération logique OUI.
===Les unités de calcul spécialisées===
Un processeur peut disposer d’unités de calcul séparées de l'unité de calcul principale, spécialisées dans les décalages, les divisions, etc. Et certaines d'entre elles sont spécialisées dans des opérations spécifiques, qui ne sont techniquement pas des opérations entières, sur des nombres entiers.
Depuis les années 90-2000, presque tous les processeurs utilisent une unité de calcul spécialisée pour les nombres flottants : la '''Floating-Point Unit''', aussi appelée FPU. En général, elle regroupe un additionneur-soustracteur flottant et un multiplieur flottant. Parfois, elle incorpore un diviseur flottant, tout dépend du processeur. Précisons que sur certains processeurs, la FPU et l'ALU entière ne vont pas à la même fréquence, pour des raisons de performance et de consommation d'énergie !
Il existe des unités de calcul spécialisées pour les calculs d'adresse. Elles ne supportent guère plus que des incrémentations/décrémentations, des additions/soustractions, et des décalages simples. Les autres opérations n'ont pas de sens avec des adresses. L'usage d'ALU spécialisées pour les adresses est un avantage sur les processeurs où les adresses ont une taille différente des données, ce qui est fréquent sur les anciens processeurs 8 bits.
De nombreux processeurs modernes disposent d'une unité de calcul spécialisée dans le calcul des conditions, tests et branchements. C’est notamment le cas sur les processeurs sans registre d'état, qui disposent de registres à prédicats. En général, les registres à prédicats sont placés à part des autres registres, dans un banc de registre séparé. L'unité de calcul normale n'est pas reliée aux registres à prédicats, alors que l'unité de calcul pour les branchements/test/conditions l'est. les registres à prédicats sont situés juste en sortie de cette unité de calcul.
==Les registres du processeur==
Après avoir vu l'unité de calcul, il est temps de passer aux registres d'un processeur. L'organisation des registres est généralement assez compliquée, avec quelques registres séparés des autres comme le registre d'état ou le ''program counter''. Les registres d'un processeur peuvent se classer en deux camps : soit ce sont des registres isolés, soit ils sont regroupés en paquets appelés banc de registres.
Un '''banc de registres''' (''register file'') est une RAM, dont chaque byte est un registre. Il regroupe un paquet de registres différents dans un seul composant, dans une seule mémoire. Dans processeur moderne, on trouve un ou plusieurs bancs de registres. La répartition des registres, à savoir quels registres sont dans le banc de registre et quels sont ceux isolés, est très variable suivant les processeurs.
[[File:Register File Simple.svg|centre|vignette|upright=1|Banc de registres simplifié.]]
===L'adressage du banc de registres===
Le banc de registre est une mémoire comme une autre, avec une entrée d'adresse qui permet de sélectionner le registre voulu. Plutot que d'adresse, nous allons parler d''''identifiant de registre'''. Le séquenceur forge l'identifiant de registre en fonction des registres sélectionnés. Dans les chapitres précédents, nous avions vu qu'il existe plusieurs méthodes pour sélectionner un registre, qui portent les noms de modes d'adressage. Et bien les modes d'adressage jouent un grand rôle dans la forge de l'identifiant de registre.
Pour rappel, sur la quasi-totalité des processeurs actuels, les registres généraux sont identifiés par un nom de registre, terme trompeur vu que ce nom est en réalité un numéro. En clair, les processeurs numérotent les registres, le numéro/nom du registre permettant de l'identifier. Par exemple, si je veux faire une addition, je dois préciser les deux registres pour les opérandes, et éventuellement le registre pour le résultat : et bien ces registres seront identifiés par un numéro. Mais tous les registres ne sont pas numérotés et ceux qui ne le sont pas sont adressés implicitement. Par exemple, le pointeur de pile sera modifié par les instructions qui manipulent la pile, sans que cela aie besoin d'être précisé par un nom de registre dans l'instruction.
Dans le cas le plus simple, les registres nommés vont dans le banc de registres, les registres adressés implicitement sont en-dehors, dans des registres isolés. L'idéntifiant de registre est alors simplement le nom de registre, le numéro. Le séquenceur extrait ce nom de registre de l'insutrction, avant de l'envoyer sur l'entrée d'adresse du banc de registre.
[[File:Adressage du banc de registres généruax.png|centre|vignette|upright=2|Adressage du banc de registres généraux]]
Dans un cas plus complexe, des registres non-nommés sont placés dans le banc de registres. Par exemple, les pointeurs de pile sont souvent placés dans le banc de registre, même s'ils sont adressés implicitement. Même des registres aussi importants que le ''program counter'' peuvent se mettre dans le banc de registre ! Nous verrons le cas du ''program counter'' dans le chapitre suivant, qui porte sur l'unité de chargement. Dans ce cas, le séquenceur forge l'identifiant de registre de lui-même. Dans le cas des registres nommés, il ajoute quelques bits aux noms de registres. Pour les registres adressés implicitement, il forge l'identifiant à partir de rien.
[[File:Adressage du banc de registre - cas général.png|centre|vignette|upright=2|Adressage du banc de registre - cas général]]
Nous verrons plus bas que dans certains cas, le nom de registre ne suffit pas à adresser un registre dans un banc de registre. Dans ce cas, le séquenceur rajoute des bits, comme dans l'exemple précédent. Tout ce qu'il faut retenir est que l'identifiant de registre est forgé par le séquenceur, qui se base entre autres sur le nom de registre s'il est présent, sur l'instruction exécutée dans le cas d'un registre adressé implicitement.
===Les registres généraux===
Pour rappel, les registres généraux peuvent mémoriser des entiers, des adresses, ou toute autre donnée codée en binaire. Ils sont souvent séparés des registres flottants sur les architectures modernes. Les registres généraux sont rassemblés dans un banc de registre dédié, appelé le '''banc de registres généraux'''. Le banc de registres généraux est une mémoire multiport, avec au moins un port d'écriture et deux ports de lecture. La raison est que les instructions lisent deux opérandes dans les registres et enregistrent leur résultat dans des registres. Le tout se marie bien avec un banc de registre à deux de lecture (pour les opérandes) et un d'écriture (pour le résultat).
[[File:Banc de registre multiports.png|centre|vignette|upright=2|Banc de registre multiports.]]
L'interface exacte dépend de si l'architecture est une architecture 2 ou 3 adresses. Pour rappel, la différence entre les deux tient dans la manière dont on précise le registre où enregistrer le résultat d'une opération. Avec les architectures 2-adresses, on précise deux registres : le premier sert à la fois comme opérande et pour mémoriser le résultat, l'autre sert uniquement d'opérande. Un des registres est donc écrasé pour enregistrer le résultat. Sur les architecture 3-adresses, on précise trois registres : deux pour les opérandes, un pour le résultat.
Les architectures 2-adresses ont un banc de registre où on doit préciser deux "adresses", deux noms de registre. L'interface du banc de registre est donc la suivante :
[[File:Register File Medium.svg|centre|vignette|upright=1.5|Register File d'une architecture à 2-adresses]]
Les architectures 3-adresses doivent rajouter une troisième entrée pour préciser un troisième nom de registre. L'interface du banc de registre est donc la suivante :
[[File:Register File Large.svg|centre|vignette|upright=1.5|Register File d'une architecture à 3-adresses]]
Rien n'empêche d'utiliser plusieurs bancs de registres sur un processeur qui utilise des registres généraux. La raison est une question d'optimisation. Au-delà d'un certain nombre de registres, il devient difficile d'utiliser un seul gros banc de registres. Il faut alors scinder le banc de registres en plusieurs bancs de registres séparés. Le problème est qu'il faut prévoir de quoi échanger des données entre les bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue du langage machine. Sur d'autres processeurs, les transferts de données entre bancs de registres se font via une instruction spéciale, souvent appelée COPY.
===Les registres flottants : banc de registre séparé ou unifié===
Passons maintenant aux registres flottants. Intuitivement, on a des registres séparés pour les entiers et les flottants. Il est alors plus simple d'utiliser un banc de registres séparé pour les nombres flottants, à côté d'un banc de registre entiers. L'avantage est que les nombres flottants et entiers n'ont pas forcément la même taille, ce qui se marie bien avec deux bancs de registres, où la taille des registres est différente dans les deux bancs.
Mais d'autres processeurs utilisent un seul '''banc de registres unifié''', qui regroupe tous les registres de données, qu'ils soient entier ou flottants. Par exemple, c'est le cas des Pentium Pro, Pentium II, Pentium III, ou des Pentium M : ces processeurs ont des registres séparés pour les flottants et les entiers, mais ils sont regroupés dans un seul banc de registres. Avec cette organisation, un registre flottant et un registre entier peuvent avoir le même nom de registre en langage machine, mais l'adresse envoyée au banc de registres ne doit pas être la même : le séquenceur ajoute des bits au nom de registre pour former l'adresse finale.
[[File:Désambiguïsation de registres sur un banc de registres unifié.png|centre|vignette|upright=2|Désambiguïsation de registres sur un banc de registres unifié.]]
===Le registre d'état===
Le registre d'état fait souvent bande à part et n'est pas placé dans un banc de registres. En effet, le registre d'état est très lié à l'unité de calcul. Il reçoit des indicateurs/''flags'' provenant de la sortie de l'unité de calcul, et met ceux-ci à disposition du reste du processeur. Son entrée est connectée à l'unité de calcul, sa sortie est reliée au séquenceur et/ou au bus interne au processeur.
Le registre d'état est relié au séquenceur afin que celui-ci puisse gérer les instructions de branchement, qui ont parfois besoin de connaitre certains bits du registre d'état pour savoir si une condition a été remplie ou non. D'autres processeurs relient aussi le registre d'état au bus interne, ce qui permet de lire son contenu et de le copier dans un registre de données. Cela permet d'implémenter certaines instructions, notamment celles qui permettent de mémoriser le registre d'état dans un registre général.
[[File:Place du registre d'état dans le chemin de données.png|centre|vignette|upright=2|Place du registre d'état dans le chemin de données]]
L'ALU fournit une sortie différente pour chaque bit du registre d'état, la connexion du registre d'état est directe, comme indiqué dans le schéma suivant. Vous remarquerez que le bit de retenue est à la fois connecté à la sortie de l'ALU, mais aussi sur son entrée. Ainsi, le bit de retenue calculé par une opération peut être utilisé pour la suivante. Sans cela, diverses instructions comme les opérations ''add with carry'' ne seraient pas possibles.
[[File:AluStatusRegister.svg|centre|vignette|upright=2|Registre d'état et unit de calcul.]]
Il est techniquement possible de mettre le registre d'état dans le banc de registre, pour économiser un registre. La principale difficulté est que les instructions doivent faire deux écritures dans le banc de registre : une pour le registre de destination, une pour le registre d'état. Soit on utilise deux ports d'écriture, soit on fait les deux écritures l'une après l'autre. Dans les deux cas, le cout en performances et en transistors n'en vaut pas le cout. D'ailleurs, je ne connais aucun processeur qui utilise cette technique.
Il faut noter que le registre d'état n'existe pas forcément en tant que tel dans le processeur. Quelques processeurs, dont le 8086 d'Intel, utilisent des bascules dispersées dans le processeur au lieu d'un vrai registre d'état. Les bascules dispersées mémorisent chacune un bit du registre d'état et sont placées là où elles sont le plus utile. Les bascules utilisées pour les branchements sont proches du séquenceur, le bascules pour les bits de retenue sont placées proche de l'ALU, etc.
===Les registres à prédicats===
Les registres à prédicats remplacent le registre d'état sur certains processeurs. Pour rappel, les registres à prédicat sont des registres de 1 bit qui mémorisent les résultats des comparaisons et instructions de test. Ils sont nommés/numérotés, mais les numéros en question sont distincts de ceux utilisés pour les registres généraux.
Ils sont placés à part, dans un banc de registres séparé. Le banc de registres à prédicats a une entrée de 1 bit connectée à l'ALU et une sortie de un bit connectée au séquenceur. Le banc de registres à prédicats est parfois relié à une unité de calcul spécialisée dans les conditions/instructions de test. Pour rappel, certaines instructions permettent de faire un ET, un OU, un XOR entre deux registres à prédicats. Pour cela, l'unité de calcul dédiée aux conditions peut lire les registres à prédicats, pour combiner le contenu de plusieurs d'entre eux.
[[File:Banc de registre pour les registres à prédicats.png|centre|vignette|upright=2|Banc de registre pour les registres à prédicats]]
===Les registres dédiés aux interruptions===
Dans le chapitre sur les registres, nous avions vu que certains processeurs dupliquaient leurs registres architecturaux, pour accélérer les interruptions ou les appels de fonction. Dans le cas qui va nous intéresser, les interruptions avaient accès à leurs propres registres, séparés des registres architecturaux. Les processeurs de ce type ont deux ensembles de registres identiques : un dédié aux interruptions, un autre pour les programmes normaux. Les registres dans les deux ensembles ont les mêmes noms, mais le processeur choisit le bon ensemble suivant s'il est dans une interruption ou non. Si on peut utiliser deux bancs de registres séparés, il est aussi possible d'utiliser un banc de registre unifié pour les deux.
Sur certains processeurs, le banc de registre est dupliqué en plusieurs exemplaires. La technique est utilisée pour les interruptions. Certains processeurs ont deux ensembles de registres identiques : un dédié aux interruptions, un autre pour les programmes normaux. Les registres dans les deux ensembles ont les mêmes noms, mais le processeur choisit le bon ensemble suivant s'il est dans une interruption ou non. On peut utiliser deux bancs de registres séparés, un pour les interruptions, et un pour les programmes.
Sur d'autres processeurs, on utilise un banc de registre unifié pour les deux ensembles de registres. Les registres pour les interruptions sont dans les adresses hautes, les registres pour les programmes dans les adresses basses. Le choix entre les deux est réalisé par un bit qui indique si on est dans une interruption ou non, disponible dans une bascule du processeur. Appelons là la bascule I.
===Le fenêtrage de registres===
[[File:Fenetre de registres.png|vignette|upright=1|Fenêtre de registres.]]
Le '''fenêtrage de registres''' fait que chaque fonction a accès à son propre ensemble de registres, sa propre fenêtre de registres. Là encore, cette technique duplique chaque registre architectural en plusieurs exemplaires qui portent le même nom. Chaque ensemble de registres architecturaux forme une fenêtre de registre, qui contient autant de registres qu'il y a de registres architecturaux. Lorsqu'une fonction s’exécute, elle se réserve une fenêtre inutilisée, et peut utiliser les registres de la fenêtre comme bon lui semble : une fonction manipule le registre architectural de la fenêtre réservée, mais pas les registres avec le même nom dans les autres fenêtres.
Il peut s'implémenter soit avec un banc de registres unifié, soit avec un banc de registre par fenêtre de registres.
Il est possible d'utiliser des bancs de registres dupliqués pour le fenêtrage de registres. Chaque fenêtre de registre a son propre banc de registres. Le choix entre le banc de registre à utiliser est fait par un registre qui mémorise le numéro de la fenêtre en cours. Ce registre commande un multiplexeur qui permet de choisir le banc de registre adéquat.
[[File:Fenêtrage de registres au niveau du banc de registres.png|vignette|Fenêtrage de registres au niveau du banc de registres.]]
L'utilisation d'un banc de registres unifié permet d'implémenter facilement le fenêtrage de registres. Il suffit pour cela de regrouper tous les registres des différentes fenêtres dans un seul banc de registres. Il suffit de faire comme vu au-dessus : rajouter des bits au nom de registre pour faire la différence entre les fenêtres. Cela implique de se souvenir dans quelle fenêtre de registre on est actuellement, cette information étant mémorisée dans un registre qui stocke le numéro de la fenêtre courante. Pour changer de fenêtre, il suffit de modifier le contenu de ce registre lors d'un appel ou retour de fonction avec un petit circuit combinatoire. Bien sûr, il faut aussi prendre en compte le cas où ce registre déborde, ce qui demande d'ajouter des circuits pour gérer la situation.
[[File:Désambiguïsation des fenêtres de registres.png|centre|vignette|upright=2|Désambiguïsation des fenêtres de registres.]]
==L'interface de communication avec la mémoire==
L''''interface avec la mémoire''' est, comme son nom l'indique, des circuits qui servent d'intermédiaire entre le bus mémoire et le processeur. Elle est parfois appelée l'unité mémoire, l'unité d'accès mémoire, la ''load-store unit'', et j'en oublie.
[[File:Unité de communication avec la mémoire, de type simple port.png|centre|vignette|upright=2|Unité de communication avec la mémoire, de type simple port.]]
Sur certains processeurs, elle gère les mémoires multiport.
[[File:Unité de communication avec la mémoire, de type multiport.png|centre|vignette|upright=2|Unité de communication avec la mémoire, de type multiport.]]
===Les registres d'interfaçage mémoire===
L'interface mémoire se résume le plus souvent à des '''registres d’interfaçage mémoire''', intercalés entre le bus mémoire et le chemin de données. Généralement, il y a au moins deux registres d’interfaçage mémoire : un registre relié au bus d'adresse, et autre relié au bus de données.
[[File:Registres d’interfaçage mémoire.png|centre|vignette|upright=2|Registres d’interfaçage mémoire.]]
Au lieu de lire ou écrire directement sur le bus, le processeur lit ou écrit dans ces registres, alors que l'unité d'accès mémoire s'occupe des échanges entre registres et bus mémoire. Lors d'une écriture, le processeur place l'adresse dans le registre d'interfaçage d'adresse, met la donnée à écrire dans le registre d'interfaçage de donnée, puis laisse l'unité d'accès mémoire faire son travail. Lors d'une lecture, il place l'adresse à lire sur le registre d'interfaçage d'adresse, il attend que la donnée soit lue, puis récupère la donnée dans le registre d'interfaçage de données.
L'avantage est que le processeur n'a pas à maintenir une donnée/adresse sur le bus durant tout un accès mémoire. Par exemple, prenons le cas où la mémoire met 15 cycles processeurs pour faire une lecture ou une écriture. Sans registres d'interfaçage mémoire, le processeur doit maintenir l'adresse durant 15 cycles, et aussi la donnée dans le cas d'une écriture. Avec ces registres, le processeur écrit dans les registres d'interfaçage mémoire au premier cycle, et passe les 14 cycles suivants à faire quelque chose d'autre. Par exemple, il faut faire un calcul en parallèle, envoyer des signaux de commande au banc de registre pour qu'il soit prêt une fois la donnée lue arrivée, etc. Cet avantage simplifie l'implémentation de certains modes d'adressage, comme on le verra à la fin du chapitre.
===L'unité de calcul d'adresse===
Les registres d'interfaçage sont presque toujours présents, mais le circuit que nous allons voir est complétement facultatif. Il s'agit d'une unité de calcul spécialisée dans les calculs d'adresse, dont nous avons parlé rapidement dans la section sur les ALU. Elle s'appelle l''''''Address generation unit''''', ou AGU. Elle est parfois séparée de l'interface mémoire proprement dit, et est alors considérée comme une unité de calcul à part, mais elle est généralement intimement liée à l'interface mémoire.
Elle sert pour certains modes d'adressage, qui demandent de combiner une adresse avec soit un indice, soit un décalage, plus rarement les deux. Les calculs d'adresse demandent de simplement incrémenter/décrémenter une adresse, de lui ajouter un indice (et de décaler les indices dans certains cas), mais guère plus. Pas besoin d'effectuer de multiplications, de divisions, ou d'autre opération plus complexe. Des décalages et des additions/soustractions suffisent. L'AGU est donc beaucoup plus simple qu'une ALU normale et se résume souvent à un vulgaire additionneur-soustracteur, éventuellement couplée à un décaleur pour multiplier les indices.
[[File:Unité d'accès mémoire avec unité de calcul dédiée.png|centre|vignette|upright=1.5|Unité d'accès mémoire avec unité de calcul dédiée]]
Le fait d'avoir une unité de calcul séparée pour les adresses peut s'expliquer pour plusieurs raisons. Sur les processeurs normaux, la raison est que cela simplifie un peu l'implémentation des modes d'adressage indirects. Sur les rares processeurs qui ont des registres séparés pour les adresses, un banc de registre dédié est réservé aux registres d'adresses, ce qui rend l'usage d'une unité de calcul d'adresse bien plus pratique. Une autre raison se manifestait sur les processeurs 8 bits : ils géraient des données de 8 bits, mais des adresses de 16 bits. Dans ce cas, le processeur avait une ALU simple de 16 bits pour les adresses, et une ALU complexe de 8 bits pour les données.
[[File:Unité d'accès mémoire avec registres d'adresse ou d'indice.png|centre|vignette|upright=2|Unité d'accès mémoire avec registres d'adresse ou d'indice]]
===La gestion de l'alignement et du boutisme===
L'interface mémoire gère les accès mémoire non-alignés, à cheval sur deux mots mémoire (rappelez-vous le chapitre sur l'alignement mémoire). Elle détecte les accès mémoire non-alignés et réagit en conséquence. Dans le cas où les accès non-alignés sont interdits, elle lève une exception matérielle. Dans le cas où ils sont autorisés, elle les gère automatiquement, à savoir qu'elle charge deux mots mémoire et les combine entre eux pour donner le résultat final. Dans les deux cas, cela demande d'ajouter des circuits de détection des accès non-alignés, et éventuellement des circuits pour le double lecture/écriture.
Les circuits de détection des accès non-alignés sont très simples. Dans le cas où les adresses sont alignées sur une puissance de deux (cas le plus courant), il suffit de vérifier les bits de poids faible de l'adresse à lire. Prenons l'exemple d'un processeur avec des adresses codées sur 64 bits, avec des mots mémoire de 32 bits, alignés sur 32 bits (4 octets). Un mot mémoire contient 4 octets, les contraintes d'alignement font que les adresses autorisées sont des multiples de 4. En conséquence, les 2 bits de poids faible d'une adresse valide sont censés être à 0. En vérifiant la valeur de ces deux bits, on détecte facilement les accès non-alignés.
En clair, détecter les accès non-alignés demande de tester si les bits de poids faibles adéquats sont à 0. Il suffit donc d'un circuit de comparaison avec zéro; qui est une simple porte OU. Cette porte OU génère un bit qui indique si l'accès testé est aligné ou non : 1 si l'accès est non-aligné, 0 sinon. Le signal peut être transmis au séquenceur pour générer une exception matérielle, ou utilisé dans l'unité d'accès mémoire pour la double lecture/écriture.
La gestion automatique des accès non-alignés est plus complexe. Dans ce cas, l'unité mémoire charge deux mots mémoire et les combine entre eux pour donner le résultat final. Charger deux mots mémoires consécutifs est assez simple, si le registre d'interfaçage est un compteur. L'accès initial charge le premier mot mémoire, puis l'adresse stockée dans le registre d'interfaçage est incrémentée pour démarrer un second accès. Le circuit pour combiner deux mots mémoire contient des registres, des circuits de décalage, des multiplexeurs.
===Le rafraichissement mémoire optimisé et le contrôleur mémoire intégré===
Depuis les années 80, les processeurs sont souvent combinés avec une mémoire principale de type DRAM. De telles mémoires doivent être rafraichies régulièrement pour ne pas perdre de données. Le rafraichissement se fait généralement adresse par adresse, ou ligne par ligne (les lignes sont des super-bytes internes à la DRAM). Le rafraichissement est en théorie géré par le contrôleur mémoire installé sur la carte mère. Mais au tout début de l'informatique, du temps des processeurs 8 bits, le rafraichissement mémoire était géré directement par le processeur.
Si quelques processeurs géraient le rafraichissement mémoire avec des interruptions, d'autres processeurs disposaient d’optimisations pour optimiser le rafraichissement mémoire. Divers processeurs implémentaient de quoi faciliter le rafraichissement par adresse. Par exemple, le processeur Zilog Z80 contenait un compteur de ligne, un registre qui contenait le numéro de la prochaine ligne à rafraichir. Il était incrémenté à chaque rafraichissement mémoire, automatiquement, par le processeur lui-même. Un ''timer'' interne permettait de savoir quand rafraichir la mémoire : quand ce ''timer'' atteignait 0, une commande de rafraichissement était envoyée à la mémoire, et le ''timer'' était ''reset''. Et tout cela était intégré à l'unité d'accès mémoire.
Depuis les années 2000, les processeurs modernes ont un contrôleur mémoire DRAM intégré directement dans le processeur. Ce qui fait qu'ils gèrent non seulement le rafraichissement, mais aussi d'autres fonctions bien pus complexes.
==Le chemin de données et son réseau d'interconnexions==
Nous venons de voir que le chemin de données contient une unité de calcul (parfois plusieurs), des registres isolés, un banc de registre, une unité mémoire. Le tout est chapeauté par une unité de contrôle qui commande le chemin de données, qui fera l'objet des prochains chapitres. Mais il faut maintenant relier registres, ALU et unité mémoire pour que l'ensemble fonctionne. Pour cela, diverses interconnexions internes au processeur se chargent de relier le tout.
Sur les anciens processeurs, les interconnexions sont assez simples et se résument à un ou deux '''bus internes au processeur''', reliés au bus mémoire. C'était la norme sur des architectures assez ancienne, qu'on n'a pas encore vu à ce point du cours, appelées les architectures à accumulateur et à pile. Mais ce n'est plus la solution utilisée actuellement. De nos jours, le réseaux d'interconnexion intra-processeur est un ensemble de connexions point à point entre ALU/registres/unité mémoire. Et paradoxalement, cela rend plus facile de comprendre ce réseau d'interconnexion.
===Introduction propédeutique : l'implémentation des modes d'adressage principaux===
L'organisation interne du processeur dépend fortement des modes d'adressage supportés. Pour simplifier les explications, nous allons séparer les modes d'adressage qui gèrent les pointeurs et les autres. Suivant que le processeur supporte les pointeurs ou non, l'organisation des bus interne est légèrement différente. La différence se voit sur les connexions avec le bus d'adresse et de données.
Tout processeur gère au minimum le '''mode d'adressage absolu''', où l'adresse est intégrée à l'instruction. Le séquenceur extrait l'adresse mémoire de l'instruction, et l'envoie sur le bus d'adresse. Pour cela, le séquenceur est relié au bus d'adresse, le chemin de donnée est relié au bus de données. Le chemin de donnée n'est pas connecté au bus d'adresse, il n'y a pas d'autres connexions.
[[File:Chemin de données sans support des pointeurs.png|centre|vignette|upright=2|Chemin de données sans support des pointeurs]]
Le '''support des pointeurs''' demande d'intégrer des modes d'adressage dédiés : l'adressage indirect à registre, l'adresse base + indice, et les autres. Les pointeurs sont stockés dans le banc de registre et sont modifiés par l'unité de calcul. Pour supporter les pointeurs, le chemin de données est connecté sur le bus d'adresse avec le séquenceur. Suivant le mode d'adressage, le bus d'adresse est relié soit au chemin de données, soit au séquenceur.
[[File:Chemin de données avec support des pointeurs.png|centre|vignette|upright=2|Chemin de données avec support des pointeurs]]
Pour terminer, il faut parler des instructions de '''copie mémoire vers mémoire''', qui copient une donnée d'une adresse mémoire vers une autre. Elles ne se passent pas vraiment dans le chemin de données, mais se passent purement au niveau des registres d’interfaçage. L'usage d'un registre d’interfaçage unique permet d'implémenter ces instructions très facilement. Elle se fait en deux étapes : on copie la donnée dans le registre d’interfaçage, on l'écrit en mémoire RAM. L'adresse envoyée sur le bus d'adresse n'est pas la même lors des deux étapes.
===Le banc de registre est multi-port, pour gérer nativement les opérations dyadiques===
Les architectures RISC et CISC incorporent un banc de registre, qui est connecté aux unités de calcul et au bus mémoire. Et ce banc de registre peut être mono-port ou multiport. S'il a existé d'anciennes architectures utilisant un banc de registre mono-port, elles sont actuellement obsolètes. Nous les aborderons dans un chapitre dédié aux architectures dites canoniques, mais nous pouvons les laisser de côté pour le moment. De nos jours, tous les processeurs utilisent un banc de registre multi-port.
[[File:Chemin de données minimal d'une architecture LOAD-STORE (sans MOV inter-registres).png|centre|vignette|upright=2|Chemin de données minimal d'une architecture LOAD-STORE (sans MOV inter-registres)]]
Le banc de registre multiport est optimisé pour les opérations dyadiques. Il dispose précisément de deux ports de lecture et d'un port d'écriture pour l'écriture. Un port de lecture par opérande et le port d'écriture pour enregistrer le résultat. En clair, le processeur peut lire deux opérandes et écrire un résultat en un seul cycle d'horloge. L'avantage est que les opérations simples ne nécessitent qu'une micro-opération, pas plus.
[[File:ALU data paths.svg|centre|vignette|upright=1.5|Processeur LOAD-STORE avec un banc de registre multiport, avec les trois ports mis en évidence.]]
===Une architecture LOAD-STORE basique, avec adressage absolu===
Voyons maintenant comment l'implémentation d'une architecture RISC très simple, qui ne supporte pas les adressages pour les pointeurs, juste les adressages inhérent (à registres) et absolu (par adresse mémoire). Les instructions LOAD et STORE utilisent l'adressage absolu, géré par le séquenceur, reste à gérer l'échange entre banc de registres et bus de données. Une lecture LOAD relie le bus de données au port d'écriture du banc de registres, alors que l'écriture relie le bus au port de lecture du banc de registre. Pour cela, il faut ajouter des multiplexeurs sur les chemins existants, comme illustré par le schéma ci-dessous.
[[File:Bus interne au processeur sur archi LOAD STORE avec banc de registres multiport.png|centre|vignette|upright=2|Organisation interne d'une architecture LOAD STORE avec banc de registres multiport. Nous n'avons pas représenté les signaux de commandes envoyés par le séquenceur au chemin de données.]]
Ajoutons ensuite les instructions de copie entre registres, souvent appelées instruction COPY ou MOV. Elles existent sur la plupart des architectures LOAD-STORE. Une première solution boucle l'entrée du banc de registres sur son entrée, ce qui ne sert que pour les copies de registres.
[[File:Chemin de données d'une architecture LOAD-STORE.png|centre|vignette|upright=2|Chemin de données d'une architecture LOAD-STORE]]
Mais il existe une seconde solution, qui ne demande pas de modifier le chemin de données. Il est possible de faire passer les copies de données entre registres par l'ALU. Lors de ces copies, l'ALU une opération ''Pass through'', à savoir qu'elle recopie une des opérandes sur sa sortie. Le fait qu'une ALU puisse effectuer une opération ''Pass through'' permet de fortement simplifier le chemin de donnée, dans le sens où cela permet d'économiser des multiplexeurs. Mais nous verrons cela sous peu. D'ailleurs, dans la suite du chapitre, nous allons partir du principe que les copies entre registres passent par l'ALU, afin de simplifier les schémas.
===L'ajout des modes d'adressage indirects à registre pour les pointeurs===
Passons maintenant à l'implémentation des modes d'adressages pour les pointeurs. Avec eux, l'adresse mémoire à lire/écrire n'est pas intégrée dans une instruction, mais est soit dans un registre, soit calculée par l'ALU.
Le premier mode d'adressage de ce type est le mode d'adressage indirect à registre, où l'adresse à lire/écrire est dans un registre. L'implémenter demande donc de connecter la sortie du banc de registres au bus d'adresse. Il suffit d'ajouter un MUX en sortie d'un port de lecture.
[[File:Chemin de données à trois bus.png|centre|vignette|upright=2|Chemin de données à trois bus.]]
Le mode d'adressage base + indice est un mode d'adressage où l'adresse à lire/écrire est calculée à partir d'une adresse et d'un indice, tous deux présents dans un registre. Le calcul de l'adresse implique au minimum une addition et donc l'ALU. Dans ce cas, on doit connecter la sortie de l'unité de calcul au bus d'adresse.
[[File:Bus avec adressage base+index.png|centre|vignette|upright=2|Bus avec adressage base+index]]
Le chemin de données précédent gère aussi le mode d'adressage indirect avec pré-décrément. Pour rappel, ce mode d'adressage est une variante du mode d'adressage indirect, qui utilise une pointeur/adresse stocké dans un registre. La différence est que ce pointeur est décrémenté avant d'être envoyé sur le bus d'adresse. L'implémentation matérielle est la même que pour le mode Base + Indice : l'adresse est lue depuis les registres, décrémentée dans l'ALU, et envoyée sur le bus d'adresse.
Le schéma précédent montre que le bus d'adresse est connecté à un MUX avant l'ALU et un autre MUX après. Mais il est possible de se passer du premier MUX, utilisé pour le mode d'adressage indirect à registre. La condition est que l'ALU supporte l'opération ''pass through'', un NOP, qui recopie une opérande sur sa sortie. L'ALU fera une opération NOP pour le mode d'adressage indirect à registre, un calcul d'adresse pour le mode d'adressage base + indice. Par contre, faire ainsi rendra l'adressage indirect légèrement plus lent, vu que le temps de passage dans l'ALU sera compté.
[[File:Bus avec adressage indirect.png|centre|vignette|upright=2|Bus avec adressages pour les pointeurs, simplifié.]]
Dans ce qui va suivre, nous allons partir du principe que le processeur est implémenté en suivant le schéma précédent, afin d'avoir des schéma plus lisibles.
===L'adressage immédiat et les modes d'adressages exotiques===
Passons maintenant au mode d’adressage immédiat, qui permet de préciser une constante dans une instruction directement. La constante est extraite de l'instruction par le séquenceur, puis insérée au bon endroit dans le chemin de données. Pour les opérations arithmétiques/logiques/branchements, il faut insérer la constante extraite sur l'entrée de l'ALU. Sur certains processeurs, la constante peut être négative et doit alors subir une extension de signe dans un circuit spécialisé.
[[File:Chemin de données - Adressage immédiat avec extension de signe.png|centre|vignette|upright=2|Chemin de données - Adressage immédiat avec extension de signe.]]
L'implémentation précédente gère aussi les modes d'adressage base + décalage et absolu indexé. Pour rappel, le premier ajoute une constante à une adresse prise dans les registres, le second prend une adresse constante et lui ajoute un indice pris dans les registres. Dans les deux cas, on lit un registre, extrait une constante/adresse de l’instruction, additionne les deux dans l'ALU, avant d'envoyer le résultat sur le bus d'adresse. La seule difficulté est de désactiver l'extension de signe pour les adresses.
Le mode d'adressage absolu peut être traité de la même manière, si l'ALU est capable de faire des NOPs. L'adresse est insérée au même endroit que pour le mode d'adressage immédiat, parcours l'unité de calcul inchangée parce que NOP, et termine sur le bus d'adresse.
[[File:Chemin de données avec une ALU capable de faire des NOP.png|centre|vignette|upright=2|Chemin de données avec adressage immédiat étendu pour gérer des adresses.]]
Passons maintenant au cas particulier d'une instruction MOV qui copie une constante dans un registre. Il n'y a rien à faire si l'unité de calcul est capable d'effectuer une opération NOP/''pass through''. Pour charger une constante dans un registre, l'ALU est configurée pour faire un NOP, la constante traverse l'ALU et se retrouve dans les registres. Si l'ALU ne gère pas les NOP, la constante doit être envoyée sur l'entrée d'écriture du banc de registres, à travers un MUX dédié.
[[File:Implémentation de l'adressage immédiat dans le chemin de données.png|centre|vignette|upright=2|Implémentation de l'adressage immédiat dans le chemin de données]]
===Les architectures CISC : les opérations ''load-op''===
Tout ce qu'on a vu précédemment porte sur les processeurs de type LOAD-STORE, souvent confondus avec les processeurs de type RISC, où les accès mémoire sont séparés des instructions utilisant l'ALU. Il est maintenant temps de voir les processeurs CISC, qui gèrent des instructions ''load-op'', qui peuvent lire une opérande depuis la mémoire.
L'implémentation des opérations ''load-op'' relie le bus de donnée directement sur une entrée de l'unité de calcul, en utilisant encore une fois un multiplexeur. L'implémentation parait simple, mais c'est parce que toute la complexité est déportée dans le séquenceur. C'est lui qui se charge de détecter quand la lecture de l'opérande est terminée, quand l'opérande est disponible.
Les instructions ''load-op'' s'exécutent en plusieurs étapes, en plusieurs micro-opérations. Il y a typiquement une étape pour l'opérande à lire en mémoire et une étape de calcul. L'usage d'un registre d’interfaçage permet d'implémenter les instructions ''load-op'' très facilement. Une opération ''load-op'' charge l'opérande en mémoire dans un registre d’interfaçage, puis relier ce registre d’interfaçage sur une des entrées de l'ALU. Un simple multiplexeur suffit pour implémenter le tout, en plus des modifications adéquates du séquenceur.
[[File:Chemin de données d'un CPU CISC avec lecture des opérandes en mémoire.png|centre|vignette|upright=2|Chemin de données d'un CPU CISC avec lecture des opérandes en mémoire]]
Supporter les instructions multi-accès (qui font plusieurs accès mémoire) ne modifie pas fondamentalement le réseau d'interconnexion, ni le chemin de données La raison est que supporter les instructions multi-accès se fait au niveau du séquenceur. En réalité, les accès mémoire se font en série, l'un après l'autre, sous la commande du séquenceur qui émet plusieurs micro-opérations mémoire consécutives. Les données lues sont placées dans des registres d’interactivement mémoire, ce qui demande d'ajouter des registres d’interfaçage mémoire en plus.
==Annexe : le cas particulier du pointeur de pile==
Le pointeur de pile est un registre un peu particulier. Il peut être placé dans le chemin de données ou dans le séquenceur, voire dans l'unité de chargement, tout dépend du processeur. Tout dépend de si le pointeur de pile gère une pile d'adresses de retour ou une pile d'appel.
===Le pointeur de pile non-adressable explicitement===
Avec une pile d'adresse de retour, le pointeur de pile n'est pas adressable explicitement, il est juste adressé implicitement par des instructions d'appel de fonction CALL et des instructions de retour de fonction RET. Le pointeur de pile est alors juste incrémenté ou décrémenté par un pas constant, il ne subit pas d'autres opérations, son adressage est implicite. Il est juste incrémenté/décrémenté par pas constants, qui sont fournis par le séquenceur. Il n'y a pas besoin de le relier au chemin de données, vu qu'il n'échange pas de données avec les autres registres. Il y a alors plusieurs solutions, mais la plus simple est de placer le pointeur de pile dans le séquenceur et de l'incrémenter par un incrémenteur dédié.
Quelques processeurs simples disposent d'une pile d'appel très limitée, où le pointeur de pile n'est pas adressable explicitement. Il est adressé implicitement par les instruction CALL, RET, mais aussi PUSH et POP, mais aucune autre instruction ne permet cela. Là encore, le pointeur de pile ne communique pas avec les autres registres. Il est juste incrémenté/décrémenté par pas constants, qui sont fournis par le séquenceur. Là encore, le plus simple est de placer le pointeur de pile dans le séquenceur et de l'incrémenter par un incrémenteur dédié.
Dans les deux cas, le pointeur de pile est placé dans l'unité de contrôle, le séquenceur, et est associé à un incrémenteur dédié. Il se trouve que cet incrémenteur est souvent partagé avec le ''program counter''. En effet, les deux sont des adresses mémoire, qui sont incrémentées et décrémentées par pas constants, ne subissent pas d'autres opérations (si ce n'est des branchements, mais passons). Les ressemblances sont suffisantes pour fusionner les deux circuits. Ils peuvent donc avoir un '''incrémenteur partagé'''.
L'incrémenteur en question est donc partagé entre pointeur de pile, ''program counter'' et quelques autres registres similaires. Par exemple, le Z80 intégrait un registre pour le rafraichissement mémoire, qui était réalisé par le CPU à l'époque. Ce registre contenait la prochaine adresse mémoire à rafraichir, et était incrémenté à chaque rafraichissement d'une adresse. Et il était lui aussi intégré au séquenceur et incrémenté par l'incrémenteur partagé.
[[File:Organisation interne d'une architecture à pile.png|centre|vignette|upright=2|Organisation interne d'une architecture à pile]]
===Le pointeur de pile adressable explicitement===
Maintenant, étudions le cas d'une pile d'appel, précisément d'une pile d'appel avec des cadres de pile de taille variable. Sous ces conditions, le pointeur de pile est un registre adressable, avec un nom/numéro de registre dédié. Tel est par exemple le cas des processeurs x86 avec le registre ESP (''Extended Stack Pointer''). 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 calculs d'adresse, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres.
Dans ce cas, la meilleure solution est de placer le pointeur de pile dans le banc de registre généraux, avec les autres registres entiers. En faisant cela, la manipulation du pointeur de pile est faite par l'unité de calcul entière, pas besoin d'utiliser un incrémenteur dédiée. Il a existé des processeurs qui mettaient le pointeur de pile dans le banc de registre, mais l'incrémentaient avec un incrémenteur dédié, mais nous les verrons dans le chapitre sur les architectures à accumulateur. La raison est que sur les processeurs concernés, les adresses ne faisaient pas la même taille que les données : c'était des processeurs 8 bits, qui géraient des adresses de 16 bits.
==Annexe : l'implémentation du système d'''aliasing'' des registres des CPU x86==
Il y a quelques chapitres, nous avions parlé du système d'''aliasing'' des registres des CPU x86. Pour rappel, il permet de donner plusieurs noms de registre pour un même registre. Plus précisément, pour un registre 64 bits, le registre complet aura un nom de registre, les 32 bits de poids faible auront leur nom de registre dédié, idem pour les 16 bits de poids faible, etc. Il est possible de faire des calculs sur ces moitiés/quarts/huitièmes de registres sans problème.
===L'''aliasing'' du 8086, pour les registres 16 bits===
[[File:Register 8086.PNG|vignette|Register 8086]]
L'implémentation de l'''aliasing'' est apparue sur les premiers CPU Intel 16 bits, notamment le 8086. En tout, ils avaient quatre registres généraux 16 bits : AX, BX, CX et DX. Ces quatre registres 16 bits étaient coupés en deux octets, chacun adressable. Par exemple, le registre AX était coupé en deux octets nommés AH et AL, chacun ayant son propre nom/numéro de registre. Les instructions d'addition/soustraction pouvaient manipuler le registre AL, ou le registre AH, ce qui modifiait les 8 bits de poids faible ou fort selon le registre choisit.
Le banc de registre ne gére que 4 registres de 16 bits, à savoir AX, BX, CX et DX. Lors d'une lecture d'un registre 8 bits, le registre 16 bit entier est lu depuis le banc de registre, mais les bits inutiles sont ignorés. Par contre, l'écriture peut se faire soit avec 16 bits d'un coup, soit pour seulement un octet. Le port d'écriture du banc de registre peut être configuré de manière à autoriser l'écriture soit sur les 16 bits du registre, soit seulement sur les 8 bits de poids faible, soit écrire dans les 8 bits de poids fort.
[[File:Port d'écriture du banc de registre du 8086.png|centre|vignette|upright=2.5|Port d'écriture du banc de registre du 8086]]
Une opération sur un registre 8 bits se passe comme suit. Premièrement, on lit le registre 16 bits complet depuis le banc de registre. Si l'on a sélectionné l'octet de poids faible, il ne se passe rien de particulier, l'opérande 16 bits est envoyée directement à l'ALU. Mais si on a sélectionné l'octet de poids fort, la valeur lue est décalée de 7 rangs pour atterrir dans les 8 octets de poids faible. Ensuite, l'unité de calcul fait un calcul avec cet opérande, un calcul 16 bits tout ce qu'il y a de plus classique. Troisièmement, le résultat est enregistré dans le banc de registre, en le configurant convenablement. La configuration précise s'il faut enregistrer le résultat dans un registre 16 bits, soit seulement dans l'octet de poids faible/fort.
Afin de simplifier le câblage, les 16 bits des registres AX/BX/CX/DX sont entrelacés d'une manière un peu particulière. Intuitivement, on s'attend à ce que les bits soient physiquement dans le même ordre que dans le registre : le bit 0 est placé à côté du bit 1, suivi par le bit 2, etc. Mais à la place, l'octet de poids fort et de poids faible sont mélangés. Deux bits consécutifs appartiennent à deux octets différents. Le tout est décrit dans le tableau ci-dessous.
{|class="wikitable"
|-
! Registre 16 bits normal
| class="f_bleu" | 15
| class="f_bleu" | 14
| class="f_bleu" | 13
| class="f_bleu" | 12
| class="f_bleu" | 11
| class="f_bleu" | 10
| class="f_bleu" | 9
| class="f_bleu" | 8
| class="f_rouge" | 7
| class="f_rouge" | 6
| class="f_rouge" | 5
| class="f_rouge" | 4
| class="f_rouge" | 3
| class="f_rouge" | 2
| class="f_rouge" | 1
| class="f_rouge" | 0
|-
! Registre 16 bits du 8086
| class="f_bleu" | 15
| class="f_rouge" | 7
| class="f_bleu" | 14
| class="f_rouge" | 6
| class="f_bleu" | 13
| class="f_rouge" | 5
| class="f_bleu" | 12
| class="f_rouge" | 4
| class="f_bleu" | 11
| class="f_rouge" | 3
| class="f_bleu" | 10
| class="f_rouge" | 2
| class="f_bleu" | 9
| class="f_rouge" | 1
| class="f_bleu" | 8
| class="f_rouge" | 0
|}
En faisant cela, le décaleur en entrée de l'ALU est bien plus simple. Il y a 8 multiplexeurs, mais le câblage est bien plus simple. Par contre, en sortie de l'ALU, il faut remettre les bits du résultat dans l'ordre adéquat, celui du registre 8086. Pour cela, les interconnexions sur le port d'écriture sont conçues pour. Il faut juste mettre les fils de sortie de l'ALU sur la bonne entrée, par besoin de multiplexeurs.
===L'''aliasing'' sur les processeurs x86 32/64 bits===
Les processeurs x86 32 et 64 bits ont un système d'''aliasing'' qui complète le système précédent. Les processeurs 32 bits étendent les registres 16 bits existants à 32 bits. Pour ce faire, le registre 32 bit a un nouveau nom de registre, distincts du nom de registre utilisé pour l'ancien registre 16 bits. Il est possible d'adresser les 16 bits de poids faible de ce registre, avec le même nom de registre que celui utilisé pour le registre 16 sur les processeurs d'avant. Même chose avec les processeurs 64, avec l'ajout d'un nouveau nom de registre pour adresser un registre de 64 bit complet.
En soit, implémenter ce système n'est pas compliqué. Prenons le cas du registre RAX (64 bits), et de ses subdivisions nommées EAX (32 bits), AX (16 bits). À l'intérieur du banc de registre, il n'y a que le registre RAX. Le banc de registre ne comprend qu'un seul nom de registre : RAX. Les subdivisions EAX et AX n'existent qu'au niveau de l'écriture dans le banc de registre. L'écriture dans le banc de registre est configurable, de manière à ne modifier que les bits adéquats. Le résultat d'un calcul de l'ALU fait 64 bits, il est envoyé sur le port d'écriture. À ce niveau, soit les 64 bits sont écrits dans le registre, soit seulement les 32/16 bits de poids faible. Le système du 8086 est préservé pour les écritures dans les 16 bits de poids faible.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les composants d'un processeur
| prevText=Les composants d'un processeur
| next=L'unité de chargement et le program counter
| nextText=L'unité de chargement et le program counter
}}
</noinclude>
0yucoe5rkzrulmhw0irn4r1n4s3bui5
745816
745814
2025-07-02T19:52:04Z
Mewtow
31375
/* Les unités de calcul spécialisées */
745816
wikitext
text/x-wiki
Comme vu précédemment, le '''chemin de donnée''' est l'ensemble des composants dans lesquels circulent les données dans le processeur. Il comprend l'unité de calcul, les registres, l'unité de communication avec la mémoire, et le ou les interconnexions qui permettent à tout ce petit monde de communiquer. Dans ce chapitre, nous allons voir ces composants en détail.
==Les unités de calcul==
Le processeur contient des circuits capables de faire des calculs arithmétiques, des opérations logiques, et des comparaisons, qui sont regroupés dans une unité de calcul appelée '''unité arithmétique et logique'''. Certains préfèrent l’appellation anglaise ''arithmetic and logic unit'', ou ALU. Par défaut, ce terme est réservé aux unités de calcul qui manipulent des nombres entiers. Les unités de calcul spécialisées pour les calculs flottants sont désignées par le terme "unité de calcul flottant", ou encore FPU (''Floating Point Unit'').
L'interface d'une unité de calcul est assez simple : on a des entrées pour les opérandes et une sortie pour le résultat du calcul. De plus, les instructions de comparaisons ou de calcul peuvent mettre à jour le registre d'état, qui est relié à une autre sortie de l’unité de calcul. Une autre entrée, l''''entrée de sélection de l'instruction''', spécifie l'opération à effectuer. Elle sert à configurer l'unité de calcul pour faire une addition et pas une multiplication, par exemple. Sur cette entrée, on envoie un numéro qui précise l'opération à effectuer. La correspondance entre ce numéro et l'opération à exécuter dépend de l'unité de calcul. Sur les processeurs où l'encodage des instructions est "simple", une partie de l'opcode de l'instruction est envoyé sur cette entrée.
[[File:Unité de calcul usuelle.png|centre|vignette|upright=2|Unité de calcul usuelle.]]
Il faut signaler que les processeurs modernes possèdent plusieurs unités de calcul, toutes reliées aux registres. Cela permet d’exécuter plusieurs calculs en même temps dans des unités de calcul différentes, afin d'augmenter les performances du processeur. Diverses technologies, abordées dans la suite du cours permettent de profiter au mieux de ces unités de calcul : pipeline, exécution dans le désordre, exécution superscalaire, jeux d'instructions VLIW, etc. Mais laissons cela de côté pour le moment.
===L'ALU entière : additions, soustractions, opérations bit à bit===
Un processeur contient plusieurs ALUs spécialisées. La principale, présente sur tous les processeurs, est l''''ALU entière'''. Elle s'occupe uniquement des opérations sur des nombres entiers, les nombres flottants sont gérés par une ALU à part. Elle gère des opérations simples : additions, soustractions, opérations bit à bit, parfois des décalages/rotations. Par contre, elle ne gère pas la multiplication et la division, qui sont prises en charge par un circuit multiplieur/diviseur à part.
L'ALU entière a déjà été vue dans un chapitre antérieur, nommé "Les unités arithmétiques et logiques entières (simples)", qui expliquait comment en concevoir une. Nous avions vu qu'une ALU entière est une sorte de circuit additionneur-soustracteur amélioré, ce qui explique qu'elle gère des opérations entières simples, mais pas la multiplication ni la division. Nous ne reviendrons pas dessus. Cependant, il y a des choses à dire sur leur intégration au processeur.
Une ALU entière gère souvent une opération particulière, qui ne fait rien et recopie simplement une de ses opérandes sur sa sortie. L'opération en question est appelée l''''opération ''Pass through''''', encore appelée opération NOP. Elle est implémentée en utilisant un simple multiplexeur, placé en sortie de l'ALU. Le fait qu'une ALU puisse effectuer une opération ''Pass through'' permet de fortement simplifier le chemin de donnée, d'économiser des multiplexeurs. Mais nous verrons cela sous peu.
[[File:ALU avec opération NOP.png|centre|vignette|upright=2|ALU avec opération NOP.]]
Avant l'invention du microprocesseur, le processeur n'était pas un circuit intégré unique. L'ALU, le séquenceur et les registres étaient dans des puces séparées. Les ALU étaient vendues séparément et manipulaient des opérandes de 4/8 bits, les ALU 4 bits étaient très fréquentes. Si on voulait créer une ALU pour des opérandes plus grandes, il fallait construire l'ALU en combinant plusieurs ALU 4/8 bits. Par exemple, l'ALU des processeurs AMD Am2900 est une ALU de 16 bits composée de plusieurs sous-ALU de 4 bits. Cette technique qui consiste à créer des unités de calcul à partir d'unités de calcul plus élémentaires s'appelle en jargon technique du '''bit slicing'''. Nous en avions parlé dans le chapitre sur les unités de calcul, aussi nous n'en reparlerons pas plus ici.
L'ALU manipule des opérandes codées sur un certain nombre de bits. Par exemple, une ALU peut manipuler des entiers codés sur 8 bits, sur 16 bits, etc. En général, la taille des opérandes de l'ALU est la même que la taille des registres. Un processeur 32 bits, avec des registres de 32 bit, a une ALU de 32 bits. C'est intuitif, et cela rend l'implémentation du processeur bien plus facile. Mais il y a quelques exceptions, où l'ALU manipule des opérandes plus petits que la taille des registres. Par exemple, de nombreux processeurs 16 bits, avec des registres de 16 bits, utilisent une ALU de 8 bits. Un autre exemple assez connu est celui du Motorola 68000, qui était un processeur 32 bits, mais dont l'ALU faisait juste 16 bits. Son successeur, le 68020, avait lui une ALU de 32 bits.
Sur de tels processeurs, les calculs sont fait en plusieurs passes. Par exemple, avec une ALU 8 bit, les opérations sur des opérandes 8 bits se font en un cycle d'horloge, celles sur 16 bits se font en deux cycles, celles en 32 en quatre, etc. Si un programme manipule assez peu d'opérandes 16/32/64 bits, la perte de performance est assez faible. Diverses techniques visent à améliorer les performances, mais elles ne font pas de miracles. Par exemple, vu que l'ALU est plus courte, il est possible de la faire fonctionner à plus haute fréquence, pour réduire la perte de performance.
Pour comprendre comme est implémenté ce système de passes, prenons l'exemple du processeur 8 bit Z80. Ses registres entiers étaient des registres de 8 bits, alors que l'ALU était de 4 bits. Les calculs étaient faits en deux phases : une qui traite les 4 bits de poids faible, une autre qui traite les 4 bits de poids fort. Pour cela, les opérandes étaient placées dans des registres de 4 bits en entrée de l'ALU, plusieurs multiplexeurs sélectionnaient les 4 bits adéquats, le résultat était mémorisé dans un registre de résultat de 8 bits, un démultiplexeur plaçait les 4 bits du résultat au bon endroit dans ce registre. L'unité de contrôle s'occupait de la commande des multiplexeurs/démultiplexeurs. Les autres processeurs 8 ou 16 bits utilisent des circuits similaires pour faire leurs calculs en plusieurs fois.
[[File:ALU du Z80.png|centre|vignette|upright=2|ALU du Z80]]
Un exemple extrême est celui des des '''processeurs sériels''' (sous-entendu ''bit-sériels''), qui utilisent une '''ALU sérielle''', qui fait leurs calculs bit par bit, un bit à la fois. S'il a existé des processeurs de 1 bit, comme le Motorola MC14500B, la majeure partie des processeurs sériels étaient des processeurs 4, 8 ou 16 bits. L'avantage de ces ALU est qu'elles utilisent peu de transistors, au détriment des performances par rapport aux processeurs non-sériels. Mais un autre avantage est qu'elles peuvent gérer des opérandes de grande taille, avec plus d'une trentaine de bits, sans trop de problèmes.
===Les circuits multiplieurs et diviseurs===
Les processeurs modernes ont une ALU pour les opérations simples (additions, décalages, opérations logiques), couplée à une ALU pour les multiplications, un circuit multiplieur séparé. Précisons qu'il ne sert pas à grand chose de fusionner le circuit multiplieur avec l'ALU, mieux vaut les garder séparés par simplicité. Les processeurs haute performance disposent systématiquement d'un circuit multiplieur et gèrent la multiplication dans leur jeu d'instruction.
Le cas de la division est plus compliqué. La présence d'un circuit multiplieur est commune, mais les circuits diviseurs sont eux très rares. Leur cout en circuit est globalement le même que pour un circuit multiplieur, mais le gain en performance est plus faible. Le gain en performance pour la multiplication est modéré car il s'agit d'une opération très fréquente, alors qu'il est très faible pour la division car celle-ci est beaucoup moins fréquente.
Pour réduire le cout en circuits, il arrive que l'ALU pour les multiplications gère à la fois la multiplication et la division. Les circuits multiplieurs et diviseurs sont en effet très similaires et partagent beaucoup de points communs. Généralement, la fusion se fait pour les multiplieurs/diviseurs itératifs.
Il existe cependant des circuits qui se passent de multiplieurs, tout en supportant la multiplication dans leur jeu d'instruction. Certains utilisent pour cela du microcode, technique qu'on verra dans deux chapitres, mais l'Intel Atom utilise une technique franchement peu ordinaire. L'Intel Atom utilise l'unité de calcul flottante pour faire les multiplications entières. Les opérandes entières sont traduites en nombres flottants, multipliés par l'unité de calcul flottante, puis le résultat est converti en un entier avec quelques corrections à la clé. Ainsi, on fait des économies de circuits, en mutualisant le multiplieur entre l'unité de calcul flottante et l'ALU entière, surtout que ce multiplieur manipule des opérandes plus courtes. Les performances sont cependant réduites comparé à l'usage d'un vrai multiplieur entier.
===Le ''barrel shifter''===
On vient d'expliquer que la présence de plusieurs ALU spécialisée est très utile pour implémenter des opérations compliquées à insérer dans une unité de calcul normale, comme la multiplication et la division. Mais les décalages sont aussi dans ce cas, de même que les rotations. Nous avions vu il y a quelques chapitres qu'ils sont réalisés par un circuit spécialisé, appelé un ''barrel shifter'', qu'il est difficile de fusionner avec une ALU normale. Aussi, beaucoup de processeurs incorporent un ''barrel shifter'' séparé de l'ALU.
Les processeurs ARM utilise un ''barrel shifter'', mais d'une manière un peu spéciale. On a vu il y a quelques chapitres que si on fait une opération logique, une addition, une soustraction ou une comparaison, la seconde opérande peut être décalée automatiquement. L'instruction incorpore le type de de décalage à faire et par combien de rangs il faut décaler directement à côté de l'opcode. Cela simplifie grandement les calculs d'adresse, qui se font en une seule instruction, contre deux ou trois sur d'autres architectures. Et pour cela, l'ALU proprement dite est précédée par un ''barrel shifter'',une seconde ALU spécialisée dans les décalages. Notons que les instructions MOV font aussi partie des instructions où la seconde opérande (le registre source) peut être décalé : cela signifie que les MOV passent par l'ALU, qui effectue alors un NOP, une opération logique OUI.
===Les unités de calcul spécialisées===
Un processeur peut disposer d’unités de calcul séparées de l'unité de calcul principale, spécialisées dans les décalages, les divisions, etc. Et certaines d'entre elles sont spécialisées dans des opérations spécifiques, qui ne sont techniquement pas des opérations entières, sur des nombres entiers.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Depuis les années 90-2000, presque tous les processeurs utilisent une unité de calcul spécialisée pour les nombres flottants : la '''Floating-Point Unit''', aussi appelée FPU. En général, elle regroupe un additionneur-soustracteur flottant et un multiplieur flottant. Parfois, elle incorpore un diviseur flottant, tout dépend du processeur. Précisons que sur certains processeurs, la FPU et l'ALU entière ne vont pas à la même fréquence, pour des raisons de performance et de consommation d'énergie !
Il existe des unités de calcul spécialisées pour les calculs d'adresse. Elles ne supportent guère plus que des incrémentations/décrémentations, des additions/soustractions, et des décalages simples. Les autres opérations n'ont pas de sens avec des adresses. L'usage d'ALU spécialisées pour les adresses est un avantage sur les processeurs où les adresses ont une taille différente des données, ce qui est fréquent sur les anciens processeurs 8 bits.
De nombreux processeurs modernes disposent d'une unité de calcul spécialisée dans le calcul des conditions, tests et branchements. C’est notamment le cas sur les processeurs sans registre d'état, qui disposent de registres à prédicats. En général, les registres à prédicats sont placés à part des autres registres, dans un banc de registre séparé. L'unité de calcul normale n'est pas reliée aux registres à prédicats, alors que l'unité de calcul pour les branchements/test/conditions l'est. les registres à prédicats sont situés juste en sortie de cette unité de calcul.
==Les registres du processeur==
Après avoir vu l'unité de calcul, il est temps de passer aux registres d'un processeur. L'organisation des registres est généralement assez compliquée, avec quelques registres séparés des autres comme le registre d'état ou le ''program counter''. Les registres d'un processeur peuvent se classer en deux camps : soit ce sont des registres isolés, soit ils sont regroupés en paquets appelés banc de registres.
Un '''banc de registres''' (''register file'') est une RAM, dont chaque byte est un registre. Il regroupe un paquet de registres différents dans un seul composant, dans une seule mémoire. Dans processeur moderne, on trouve un ou plusieurs bancs de registres. La répartition des registres, à savoir quels registres sont dans le banc de registre et quels sont ceux isolés, est très variable suivant les processeurs.
[[File:Register File Simple.svg|centre|vignette|upright=1|Banc de registres simplifié.]]
===L'adressage du banc de registres===
Le banc de registre est une mémoire comme une autre, avec une entrée d'adresse qui permet de sélectionner le registre voulu. Plutot que d'adresse, nous allons parler d''''identifiant de registre'''. Le séquenceur forge l'identifiant de registre en fonction des registres sélectionnés. Dans les chapitres précédents, nous avions vu qu'il existe plusieurs méthodes pour sélectionner un registre, qui portent les noms de modes d'adressage. Et bien les modes d'adressage jouent un grand rôle dans la forge de l'identifiant de registre.
Pour rappel, sur la quasi-totalité des processeurs actuels, les registres généraux sont identifiés par un nom de registre, terme trompeur vu que ce nom est en réalité un numéro. En clair, les processeurs numérotent les registres, le numéro/nom du registre permettant de l'identifier. Par exemple, si je veux faire une addition, je dois préciser les deux registres pour les opérandes, et éventuellement le registre pour le résultat : et bien ces registres seront identifiés par un numéro. Mais tous les registres ne sont pas numérotés et ceux qui ne le sont pas sont adressés implicitement. Par exemple, le pointeur de pile sera modifié par les instructions qui manipulent la pile, sans que cela aie besoin d'être précisé par un nom de registre dans l'instruction.
Dans le cas le plus simple, les registres nommés vont dans le banc de registres, les registres adressés implicitement sont en-dehors, dans des registres isolés. L'idéntifiant de registre est alors simplement le nom de registre, le numéro. Le séquenceur extrait ce nom de registre de l'insutrction, avant de l'envoyer sur l'entrée d'adresse du banc de registre.
[[File:Adressage du banc de registres généruax.png|centre|vignette|upright=2|Adressage du banc de registres généraux]]
Dans un cas plus complexe, des registres non-nommés sont placés dans le banc de registres. Par exemple, les pointeurs de pile sont souvent placés dans le banc de registre, même s'ils sont adressés implicitement. Même des registres aussi importants que le ''program counter'' peuvent se mettre dans le banc de registre ! Nous verrons le cas du ''program counter'' dans le chapitre suivant, qui porte sur l'unité de chargement. Dans ce cas, le séquenceur forge l'identifiant de registre de lui-même. Dans le cas des registres nommés, il ajoute quelques bits aux noms de registres. Pour les registres adressés implicitement, il forge l'identifiant à partir de rien.
[[File:Adressage du banc de registre - cas général.png|centre|vignette|upright=2|Adressage du banc de registre - cas général]]
Nous verrons plus bas que dans certains cas, le nom de registre ne suffit pas à adresser un registre dans un banc de registre. Dans ce cas, le séquenceur rajoute des bits, comme dans l'exemple précédent. Tout ce qu'il faut retenir est que l'identifiant de registre est forgé par le séquenceur, qui se base entre autres sur le nom de registre s'il est présent, sur l'instruction exécutée dans le cas d'un registre adressé implicitement.
===Les registres généraux===
Pour rappel, les registres généraux peuvent mémoriser des entiers, des adresses, ou toute autre donnée codée en binaire. Ils sont souvent séparés des registres flottants sur les architectures modernes. Les registres généraux sont rassemblés dans un banc de registre dédié, appelé le '''banc de registres généraux'''. Le banc de registres généraux est une mémoire multiport, avec au moins un port d'écriture et deux ports de lecture. La raison est que les instructions lisent deux opérandes dans les registres et enregistrent leur résultat dans des registres. Le tout se marie bien avec un banc de registre à deux de lecture (pour les opérandes) et un d'écriture (pour le résultat).
[[File:Banc de registre multiports.png|centre|vignette|upright=2|Banc de registre multiports.]]
L'interface exacte dépend de si l'architecture est une architecture 2 ou 3 adresses. Pour rappel, la différence entre les deux tient dans la manière dont on précise le registre où enregistrer le résultat d'une opération. Avec les architectures 2-adresses, on précise deux registres : le premier sert à la fois comme opérande et pour mémoriser le résultat, l'autre sert uniquement d'opérande. Un des registres est donc écrasé pour enregistrer le résultat. Sur les architecture 3-adresses, on précise trois registres : deux pour les opérandes, un pour le résultat.
Les architectures 2-adresses ont un banc de registre où on doit préciser deux "adresses", deux noms de registre. L'interface du banc de registre est donc la suivante :
[[File:Register File Medium.svg|centre|vignette|upright=1.5|Register File d'une architecture à 2-adresses]]
Les architectures 3-adresses doivent rajouter une troisième entrée pour préciser un troisième nom de registre. L'interface du banc de registre est donc la suivante :
[[File:Register File Large.svg|centre|vignette|upright=1.5|Register File d'une architecture à 3-adresses]]
Rien n'empêche d'utiliser plusieurs bancs de registres sur un processeur qui utilise des registres généraux. La raison est une question d'optimisation. Au-delà d'un certain nombre de registres, il devient difficile d'utiliser un seul gros banc de registres. Il faut alors scinder le banc de registres en plusieurs bancs de registres séparés. Le problème est qu'il faut prévoir de quoi échanger des données entre les bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue du langage machine. Sur d'autres processeurs, les transferts de données entre bancs de registres se font via une instruction spéciale, souvent appelée COPY.
===Les registres flottants : banc de registre séparé ou unifié===
Passons maintenant aux registres flottants. Intuitivement, on a des registres séparés pour les entiers et les flottants. Il est alors plus simple d'utiliser un banc de registres séparé pour les nombres flottants, à côté d'un banc de registre entiers. L'avantage est que les nombres flottants et entiers n'ont pas forcément la même taille, ce qui se marie bien avec deux bancs de registres, où la taille des registres est différente dans les deux bancs.
Mais d'autres processeurs utilisent un seul '''banc de registres unifié''', qui regroupe tous les registres de données, qu'ils soient entier ou flottants. Par exemple, c'est le cas des Pentium Pro, Pentium II, Pentium III, ou des Pentium M : ces processeurs ont des registres séparés pour les flottants et les entiers, mais ils sont regroupés dans un seul banc de registres. Avec cette organisation, un registre flottant et un registre entier peuvent avoir le même nom de registre en langage machine, mais l'adresse envoyée au banc de registres ne doit pas être la même : le séquenceur ajoute des bits au nom de registre pour former l'adresse finale.
[[File:Désambiguïsation de registres sur un banc de registres unifié.png|centre|vignette|upright=2|Désambiguïsation de registres sur un banc de registres unifié.]]
===Le registre d'état===
Le registre d'état fait souvent bande à part et n'est pas placé dans un banc de registres. En effet, le registre d'état est très lié à l'unité de calcul. Il reçoit des indicateurs/''flags'' provenant de la sortie de l'unité de calcul, et met ceux-ci à disposition du reste du processeur. Son entrée est connectée à l'unité de calcul, sa sortie est reliée au séquenceur et/ou au bus interne au processeur.
Le registre d'état est relié au séquenceur afin que celui-ci puisse gérer les instructions de branchement, qui ont parfois besoin de connaitre certains bits du registre d'état pour savoir si une condition a été remplie ou non. D'autres processeurs relient aussi le registre d'état au bus interne, ce qui permet de lire son contenu et de le copier dans un registre de données. Cela permet d'implémenter certaines instructions, notamment celles qui permettent de mémoriser le registre d'état dans un registre général.
[[File:Place du registre d'état dans le chemin de données.png|centre|vignette|upright=2|Place du registre d'état dans le chemin de données]]
L'ALU fournit une sortie différente pour chaque bit du registre d'état, la connexion du registre d'état est directe, comme indiqué dans le schéma suivant. Vous remarquerez que le bit de retenue est à la fois connecté à la sortie de l'ALU, mais aussi sur son entrée. Ainsi, le bit de retenue calculé par une opération peut être utilisé pour la suivante. Sans cela, diverses instructions comme les opérations ''add with carry'' ne seraient pas possibles.
[[File:AluStatusRegister.svg|centre|vignette|upright=2|Registre d'état et unit de calcul.]]
Il est techniquement possible de mettre le registre d'état dans le banc de registre, pour économiser un registre. La principale difficulté est que les instructions doivent faire deux écritures dans le banc de registre : une pour le registre de destination, une pour le registre d'état. Soit on utilise deux ports d'écriture, soit on fait les deux écritures l'une après l'autre. Dans les deux cas, le cout en performances et en transistors n'en vaut pas le cout. D'ailleurs, je ne connais aucun processeur qui utilise cette technique.
Il faut noter que le registre d'état n'existe pas forcément en tant que tel dans le processeur. Quelques processeurs, dont le 8086 d'Intel, utilisent des bascules dispersées dans le processeur au lieu d'un vrai registre d'état. Les bascules dispersées mémorisent chacune un bit du registre d'état et sont placées là où elles sont le plus utile. Les bascules utilisées pour les branchements sont proches du séquenceur, le bascules pour les bits de retenue sont placées proche de l'ALU, etc.
===Les registres à prédicats===
Les registres à prédicats remplacent le registre d'état sur certains processeurs. Pour rappel, les registres à prédicat sont des registres de 1 bit qui mémorisent les résultats des comparaisons et instructions de test. Ils sont nommés/numérotés, mais les numéros en question sont distincts de ceux utilisés pour les registres généraux.
Ils sont placés à part, dans un banc de registres séparé. Le banc de registres à prédicats a une entrée de 1 bit connectée à l'ALU et une sortie de un bit connectée au séquenceur. Le banc de registres à prédicats est parfois relié à une unité de calcul spécialisée dans les conditions/instructions de test. Pour rappel, certaines instructions permettent de faire un ET, un OU, un XOR entre deux registres à prédicats. Pour cela, l'unité de calcul dédiée aux conditions peut lire les registres à prédicats, pour combiner le contenu de plusieurs d'entre eux.
[[File:Banc de registre pour les registres à prédicats.png|centre|vignette|upright=2|Banc de registre pour les registres à prédicats]]
===Les registres dédiés aux interruptions===
Dans le chapitre sur les registres, nous avions vu que certains processeurs dupliquaient leurs registres architecturaux, pour accélérer les interruptions ou les appels de fonction. Dans le cas qui va nous intéresser, les interruptions avaient accès à leurs propres registres, séparés des registres architecturaux. Les processeurs de ce type ont deux ensembles de registres identiques : un dédié aux interruptions, un autre pour les programmes normaux. Les registres dans les deux ensembles ont les mêmes noms, mais le processeur choisit le bon ensemble suivant s'il est dans une interruption ou non. Si on peut utiliser deux bancs de registres séparés, il est aussi possible d'utiliser un banc de registre unifié pour les deux.
Sur certains processeurs, le banc de registre est dupliqué en plusieurs exemplaires. La technique est utilisée pour les interruptions. Certains processeurs ont deux ensembles de registres identiques : un dédié aux interruptions, un autre pour les programmes normaux. Les registres dans les deux ensembles ont les mêmes noms, mais le processeur choisit le bon ensemble suivant s'il est dans une interruption ou non. On peut utiliser deux bancs de registres séparés, un pour les interruptions, et un pour les programmes.
Sur d'autres processeurs, on utilise un banc de registre unifié pour les deux ensembles de registres. Les registres pour les interruptions sont dans les adresses hautes, les registres pour les programmes dans les adresses basses. Le choix entre les deux est réalisé par un bit qui indique si on est dans une interruption ou non, disponible dans une bascule du processeur. Appelons là la bascule I.
===Le fenêtrage de registres===
[[File:Fenetre de registres.png|vignette|upright=1|Fenêtre de registres.]]
Le '''fenêtrage de registres''' fait que chaque fonction a accès à son propre ensemble de registres, sa propre fenêtre de registres. Là encore, cette technique duplique chaque registre architectural en plusieurs exemplaires qui portent le même nom. Chaque ensemble de registres architecturaux forme une fenêtre de registre, qui contient autant de registres qu'il y a de registres architecturaux. Lorsqu'une fonction s’exécute, elle se réserve une fenêtre inutilisée, et peut utiliser les registres de la fenêtre comme bon lui semble : une fonction manipule le registre architectural de la fenêtre réservée, mais pas les registres avec le même nom dans les autres fenêtres.
Il peut s'implémenter soit avec un banc de registres unifié, soit avec un banc de registre par fenêtre de registres.
Il est possible d'utiliser des bancs de registres dupliqués pour le fenêtrage de registres. Chaque fenêtre de registre a son propre banc de registres. Le choix entre le banc de registre à utiliser est fait par un registre qui mémorise le numéro de la fenêtre en cours. Ce registre commande un multiplexeur qui permet de choisir le banc de registre adéquat.
[[File:Fenêtrage de registres au niveau du banc de registres.png|vignette|Fenêtrage de registres au niveau du banc de registres.]]
L'utilisation d'un banc de registres unifié permet d'implémenter facilement le fenêtrage de registres. Il suffit pour cela de regrouper tous les registres des différentes fenêtres dans un seul banc de registres. Il suffit de faire comme vu au-dessus : rajouter des bits au nom de registre pour faire la différence entre les fenêtres. Cela implique de se souvenir dans quelle fenêtre de registre on est actuellement, cette information étant mémorisée dans un registre qui stocke le numéro de la fenêtre courante. Pour changer de fenêtre, il suffit de modifier le contenu de ce registre lors d'un appel ou retour de fonction avec un petit circuit combinatoire. Bien sûr, il faut aussi prendre en compte le cas où ce registre déborde, ce qui demande d'ajouter des circuits pour gérer la situation.
[[File:Désambiguïsation des fenêtres de registres.png|centre|vignette|upright=2|Désambiguïsation des fenêtres de registres.]]
==L'interface de communication avec la mémoire==
L''''interface avec la mémoire''' est, comme son nom l'indique, des circuits qui servent d'intermédiaire entre le bus mémoire et le processeur. Elle est parfois appelée l'unité mémoire, l'unité d'accès mémoire, la ''load-store unit'', et j'en oublie.
[[File:Unité de communication avec la mémoire, de type simple port.png|centre|vignette|upright=2|Unité de communication avec la mémoire, de type simple port.]]
Sur certains processeurs, elle gère les mémoires multiport.
[[File:Unité de communication avec la mémoire, de type multiport.png|centre|vignette|upright=2|Unité de communication avec la mémoire, de type multiport.]]
===Les registres d'interfaçage mémoire===
L'interface mémoire se résume le plus souvent à des '''registres d’interfaçage mémoire''', intercalés entre le bus mémoire et le chemin de données. Généralement, il y a au moins deux registres d’interfaçage mémoire : un registre relié au bus d'adresse, et autre relié au bus de données.
[[File:Registres d’interfaçage mémoire.png|centre|vignette|upright=2|Registres d’interfaçage mémoire.]]
Au lieu de lire ou écrire directement sur le bus, le processeur lit ou écrit dans ces registres, alors que l'unité d'accès mémoire s'occupe des échanges entre registres et bus mémoire. Lors d'une écriture, le processeur place l'adresse dans le registre d'interfaçage d'adresse, met la donnée à écrire dans le registre d'interfaçage de donnée, puis laisse l'unité d'accès mémoire faire son travail. Lors d'une lecture, il place l'adresse à lire sur le registre d'interfaçage d'adresse, il attend que la donnée soit lue, puis récupère la donnée dans le registre d'interfaçage de données.
L'avantage est que le processeur n'a pas à maintenir une donnée/adresse sur le bus durant tout un accès mémoire. Par exemple, prenons le cas où la mémoire met 15 cycles processeurs pour faire une lecture ou une écriture. Sans registres d'interfaçage mémoire, le processeur doit maintenir l'adresse durant 15 cycles, et aussi la donnée dans le cas d'une écriture. Avec ces registres, le processeur écrit dans les registres d'interfaçage mémoire au premier cycle, et passe les 14 cycles suivants à faire quelque chose d'autre. Par exemple, il faut faire un calcul en parallèle, envoyer des signaux de commande au banc de registre pour qu'il soit prêt une fois la donnée lue arrivée, etc. Cet avantage simplifie l'implémentation de certains modes d'adressage, comme on le verra à la fin du chapitre.
===L'unité de calcul d'adresse===
Les registres d'interfaçage sont presque toujours présents, mais le circuit que nous allons voir est complétement facultatif. Il s'agit d'une unité de calcul spécialisée dans les calculs d'adresse, dont nous avons parlé rapidement dans la section sur les ALU. Elle s'appelle l''''''Address generation unit''''', ou AGU. Elle est parfois séparée de l'interface mémoire proprement dit, et est alors considérée comme une unité de calcul à part, mais elle est généralement intimement liée à l'interface mémoire.
Elle sert pour certains modes d'adressage, qui demandent de combiner une adresse avec soit un indice, soit un décalage, plus rarement les deux. Les calculs d'adresse demandent de simplement incrémenter/décrémenter une adresse, de lui ajouter un indice (et de décaler les indices dans certains cas), mais guère plus. Pas besoin d'effectuer de multiplications, de divisions, ou d'autre opération plus complexe. Des décalages et des additions/soustractions suffisent. L'AGU est donc beaucoup plus simple qu'une ALU normale et se résume souvent à un vulgaire additionneur-soustracteur, éventuellement couplée à un décaleur pour multiplier les indices.
[[File:Unité d'accès mémoire avec unité de calcul dédiée.png|centre|vignette|upright=1.5|Unité d'accès mémoire avec unité de calcul dédiée]]
Le fait d'avoir une unité de calcul séparée pour les adresses peut s'expliquer pour plusieurs raisons. Sur les processeurs normaux, la raison est que cela simplifie un peu l'implémentation des modes d'adressage indirects. Sur les rares processeurs qui ont des registres séparés pour les adresses, un banc de registre dédié est réservé aux registres d'adresses, ce qui rend l'usage d'une unité de calcul d'adresse bien plus pratique. Une autre raison se manifestait sur les processeurs 8 bits : ils géraient des données de 8 bits, mais des adresses de 16 bits. Dans ce cas, le processeur avait une ALU simple de 16 bits pour les adresses, et une ALU complexe de 8 bits pour les données.
[[File:Unité d'accès mémoire avec registres d'adresse ou d'indice.png|centre|vignette|upright=2|Unité d'accès mémoire avec registres d'adresse ou d'indice]]
===La gestion de l'alignement et du boutisme===
L'interface mémoire gère les accès mémoire non-alignés, à cheval sur deux mots mémoire (rappelez-vous le chapitre sur l'alignement mémoire). Elle détecte les accès mémoire non-alignés et réagit en conséquence. Dans le cas où les accès non-alignés sont interdits, elle lève une exception matérielle. Dans le cas où ils sont autorisés, elle les gère automatiquement, à savoir qu'elle charge deux mots mémoire et les combine entre eux pour donner le résultat final. Dans les deux cas, cela demande d'ajouter des circuits de détection des accès non-alignés, et éventuellement des circuits pour le double lecture/écriture.
Les circuits de détection des accès non-alignés sont très simples. Dans le cas où les adresses sont alignées sur une puissance de deux (cas le plus courant), il suffit de vérifier les bits de poids faible de l'adresse à lire. Prenons l'exemple d'un processeur avec des adresses codées sur 64 bits, avec des mots mémoire de 32 bits, alignés sur 32 bits (4 octets). Un mot mémoire contient 4 octets, les contraintes d'alignement font que les adresses autorisées sont des multiples de 4. En conséquence, les 2 bits de poids faible d'une adresse valide sont censés être à 0. En vérifiant la valeur de ces deux bits, on détecte facilement les accès non-alignés.
En clair, détecter les accès non-alignés demande de tester si les bits de poids faibles adéquats sont à 0. Il suffit donc d'un circuit de comparaison avec zéro; qui est une simple porte OU. Cette porte OU génère un bit qui indique si l'accès testé est aligné ou non : 1 si l'accès est non-aligné, 0 sinon. Le signal peut être transmis au séquenceur pour générer une exception matérielle, ou utilisé dans l'unité d'accès mémoire pour la double lecture/écriture.
La gestion automatique des accès non-alignés est plus complexe. Dans ce cas, l'unité mémoire charge deux mots mémoire et les combine entre eux pour donner le résultat final. Charger deux mots mémoires consécutifs est assez simple, si le registre d'interfaçage est un compteur. L'accès initial charge le premier mot mémoire, puis l'adresse stockée dans le registre d'interfaçage est incrémentée pour démarrer un second accès. Le circuit pour combiner deux mots mémoire contient des registres, des circuits de décalage, des multiplexeurs.
===Le rafraichissement mémoire optimisé et le contrôleur mémoire intégré===
Depuis les années 80, les processeurs sont souvent combinés avec une mémoire principale de type DRAM. De telles mémoires doivent être rafraichies régulièrement pour ne pas perdre de données. Le rafraichissement se fait généralement adresse par adresse, ou ligne par ligne (les lignes sont des super-bytes internes à la DRAM). Le rafraichissement est en théorie géré par le contrôleur mémoire installé sur la carte mère. Mais au tout début de l'informatique, du temps des processeurs 8 bits, le rafraichissement mémoire était géré directement par le processeur.
Si quelques processeurs géraient le rafraichissement mémoire avec des interruptions, d'autres processeurs disposaient d’optimisations pour optimiser le rafraichissement mémoire. Divers processeurs implémentaient de quoi faciliter le rafraichissement par adresse. Par exemple, le processeur Zilog Z80 contenait un compteur de ligne, un registre qui contenait le numéro de la prochaine ligne à rafraichir. Il était incrémenté à chaque rafraichissement mémoire, automatiquement, par le processeur lui-même. Un ''timer'' interne permettait de savoir quand rafraichir la mémoire : quand ce ''timer'' atteignait 0, une commande de rafraichissement était envoyée à la mémoire, et le ''timer'' était ''reset''. Et tout cela était intégré à l'unité d'accès mémoire.
Depuis les années 2000, les processeurs modernes ont un contrôleur mémoire DRAM intégré directement dans le processeur. Ce qui fait qu'ils gèrent non seulement le rafraichissement, mais aussi d'autres fonctions bien pus complexes.
==Le chemin de données et son réseau d'interconnexions==
Nous venons de voir que le chemin de données contient une unité de calcul (parfois plusieurs), des registres isolés, un banc de registre, une unité mémoire. Le tout est chapeauté par une unité de contrôle qui commande le chemin de données, qui fera l'objet des prochains chapitres. Mais il faut maintenant relier registres, ALU et unité mémoire pour que l'ensemble fonctionne. Pour cela, diverses interconnexions internes au processeur se chargent de relier le tout.
Sur les anciens processeurs, les interconnexions sont assez simples et se résument à un ou deux '''bus internes au processeur''', reliés au bus mémoire. C'était la norme sur des architectures assez ancienne, qu'on n'a pas encore vu à ce point du cours, appelées les architectures à accumulateur et à pile. Mais ce n'est plus la solution utilisée actuellement. De nos jours, le réseaux d'interconnexion intra-processeur est un ensemble de connexions point à point entre ALU/registres/unité mémoire. Et paradoxalement, cela rend plus facile de comprendre ce réseau d'interconnexion.
===Introduction propédeutique : l'implémentation des modes d'adressage principaux===
L'organisation interne du processeur dépend fortement des modes d'adressage supportés. Pour simplifier les explications, nous allons séparer les modes d'adressage qui gèrent les pointeurs et les autres. Suivant que le processeur supporte les pointeurs ou non, l'organisation des bus interne est légèrement différente. La différence se voit sur les connexions avec le bus d'adresse et de données.
Tout processeur gère au minimum le '''mode d'adressage absolu''', où l'adresse est intégrée à l'instruction. Le séquenceur extrait l'adresse mémoire de l'instruction, et l'envoie sur le bus d'adresse. Pour cela, le séquenceur est relié au bus d'adresse, le chemin de donnée est relié au bus de données. Le chemin de donnée n'est pas connecté au bus d'adresse, il n'y a pas d'autres connexions.
[[File:Chemin de données sans support des pointeurs.png|centre|vignette|upright=2|Chemin de données sans support des pointeurs]]
Le '''support des pointeurs''' demande d'intégrer des modes d'adressage dédiés : l'adressage indirect à registre, l'adresse base + indice, et les autres. Les pointeurs sont stockés dans le banc de registre et sont modifiés par l'unité de calcul. Pour supporter les pointeurs, le chemin de données est connecté sur le bus d'adresse avec le séquenceur. Suivant le mode d'adressage, le bus d'adresse est relié soit au chemin de données, soit au séquenceur.
[[File:Chemin de données avec support des pointeurs.png|centre|vignette|upright=2|Chemin de données avec support des pointeurs]]
Pour terminer, il faut parler des instructions de '''copie mémoire vers mémoire''', qui copient une donnée d'une adresse mémoire vers une autre. Elles ne se passent pas vraiment dans le chemin de données, mais se passent purement au niveau des registres d’interfaçage. L'usage d'un registre d’interfaçage unique permet d'implémenter ces instructions très facilement. Elle se fait en deux étapes : on copie la donnée dans le registre d’interfaçage, on l'écrit en mémoire RAM. L'adresse envoyée sur le bus d'adresse n'est pas la même lors des deux étapes.
===Le banc de registre est multi-port, pour gérer nativement les opérations dyadiques===
Les architectures RISC et CISC incorporent un banc de registre, qui est connecté aux unités de calcul et au bus mémoire. Et ce banc de registre peut être mono-port ou multiport. S'il a existé d'anciennes architectures utilisant un banc de registre mono-port, elles sont actuellement obsolètes. Nous les aborderons dans un chapitre dédié aux architectures dites canoniques, mais nous pouvons les laisser de côté pour le moment. De nos jours, tous les processeurs utilisent un banc de registre multi-port.
[[File:Chemin de données minimal d'une architecture LOAD-STORE (sans MOV inter-registres).png|centre|vignette|upright=2|Chemin de données minimal d'une architecture LOAD-STORE (sans MOV inter-registres)]]
Le banc de registre multiport est optimisé pour les opérations dyadiques. Il dispose précisément de deux ports de lecture et d'un port d'écriture pour l'écriture. Un port de lecture par opérande et le port d'écriture pour enregistrer le résultat. En clair, le processeur peut lire deux opérandes et écrire un résultat en un seul cycle d'horloge. L'avantage est que les opérations simples ne nécessitent qu'une micro-opération, pas plus.
[[File:ALU data paths.svg|centre|vignette|upright=1.5|Processeur LOAD-STORE avec un banc de registre multiport, avec les trois ports mis en évidence.]]
===Une architecture LOAD-STORE basique, avec adressage absolu===
Voyons maintenant comment l'implémentation d'une architecture RISC très simple, qui ne supporte pas les adressages pour les pointeurs, juste les adressages inhérent (à registres) et absolu (par adresse mémoire). Les instructions LOAD et STORE utilisent l'adressage absolu, géré par le séquenceur, reste à gérer l'échange entre banc de registres et bus de données. Une lecture LOAD relie le bus de données au port d'écriture du banc de registres, alors que l'écriture relie le bus au port de lecture du banc de registre. Pour cela, il faut ajouter des multiplexeurs sur les chemins existants, comme illustré par le schéma ci-dessous.
[[File:Bus interne au processeur sur archi LOAD STORE avec banc de registres multiport.png|centre|vignette|upright=2|Organisation interne d'une architecture LOAD STORE avec banc de registres multiport. Nous n'avons pas représenté les signaux de commandes envoyés par le séquenceur au chemin de données.]]
Ajoutons ensuite les instructions de copie entre registres, souvent appelées instruction COPY ou MOV. Elles existent sur la plupart des architectures LOAD-STORE. Une première solution boucle l'entrée du banc de registres sur son entrée, ce qui ne sert que pour les copies de registres.
[[File:Chemin de données d'une architecture LOAD-STORE.png|centre|vignette|upright=2|Chemin de données d'une architecture LOAD-STORE]]
Mais il existe une seconde solution, qui ne demande pas de modifier le chemin de données. Il est possible de faire passer les copies de données entre registres par l'ALU. Lors de ces copies, l'ALU une opération ''Pass through'', à savoir qu'elle recopie une des opérandes sur sa sortie. Le fait qu'une ALU puisse effectuer une opération ''Pass through'' permet de fortement simplifier le chemin de donnée, dans le sens où cela permet d'économiser des multiplexeurs. Mais nous verrons cela sous peu. D'ailleurs, dans la suite du chapitre, nous allons partir du principe que les copies entre registres passent par l'ALU, afin de simplifier les schémas.
===L'ajout des modes d'adressage indirects à registre pour les pointeurs===
Passons maintenant à l'implémentation des modes d'adressages pour les pointeurs. Avec eux, l'adresse mémoire à lire/écrire n'est pas intégrée dans une instruction, mais est soit dans un registre, soit calculée par l'ALU.
Le premier mode d'adressage de ce type est le mode d'adressage indirect à registre, où l'adresse à lire/écrire est dans un registre. L'implémenter demande donc de connecter la sortie du banc de registres au bus d'adresse. Il suffit d'ajouter un MUX en sortie d'un port de lecture.
[[File:Chemin de données à trois bus.png|centre|vignette|upright=2|Chemin de données à trois bus.]]
Le mode d'adressage base + indice est un mode d'adressage où l'adresse à lire/écrire est calculée à partir d'une adresse et d'un indice, tous deux présents dans un registre. Le calcul de l'adresse implique au minimum une addition et donc l'ALU. Dans ce cas, on doit connecter la sortie de l'unité de calcul au bus d'adresse.
[[File:Bus avec adressage base+index.png|centre|vignette|upright=2|Bus avec adressage base+index]]
Le chemin de données précédent gère aussi le mode d'adressage indirect avec pré-décrément. Pour rappel, ce mode d'adressage est une variante du mode d'adressage indirect, qui utilise une pointeur/adresse stocké dans un registre. La différence est que ce pointeur est décrémenté avant d'être envoyé sur le bus d'adresse. L'implémentation matérielle est la même que pour le mode Base + Indice : l'adresse est lue depuis les registres, décrémentée dans l'ALU, et envoyée sur le bus d'adresse.
Le schéma précédent montre que le bus d'adresse est connecté à un MUX avant l'ALU et un autre MUX après. Mais il est possible de se passer du premier MUX, utilisé pour le mode d'adressage indirect à registre. La condition est que l'ALU supporte l'opération ''pass through'', un NOP, qui recopie une opérande sur sa sortie. L'ALU fera une opération NOP pour le mode d'adressage indirect à registre, un calcul d'adresse pour le mode d'adressage base + indice. Par contre, faire ainsi rendra l'adressage indirect légèrement plus lent, vu que le temps de passage dans l'ALU sera compté.
[[File:Bus avec adressage indirect.png|centre|vignette|upright=2|Bus avec adressages pour les pointeurs, simplifié.]]
Dans ce qui va suivre, nous allons partir du principe que le processeur est implémenté en suivant le schéma précédent, afin d'avoir des schéma plus lisibles.
===L'adressage immédiat et les modes d'adressages exotiques===
Passons maintenant au mode d’adressage immédiat, qui permet de préciser une constante dans une instruction directement. La constante est extraite de l'instruction par le séquenceur, puis insérée au bon endroit dans le chemin de données. Pour les opérations arithmétiques/logiques/branchements, il faut insérer la constante extraite sur l'entrée de l'ALU. Sur certains processeurs, la constante peut être négative et doit alors subir une extension de signe dans un circuit spécialisé.
[[File:Chemin de données - Adressage immédiat avec extension de signe.png|centre|vignette|upright=2|Chemin de données - Adressage immédiat avec extension de signe.]]
L'implémentation précédente gère aussi les modes d'adressage base + décalage et absolu indexé. Pour rappel, le premier ajoute une constante à une adresse prise dans les registres, le second prend une adresse constante et lui ajoute un indice pris dans les registres. Dans les deux cas, on lit un registre, extrait une constante/adresse de l’instruction, additionne les deux dans l'ALU, avant d'envoyer le résultat sur le bus d'adresse. La seule difficulté est de désactiver l'extension de signe pour les adresses.
Le mode d'adressage absolu peut être traité de la même manière, si l'ALU est capable de faire des NOPs. L'adresse est insérée au même endroit que pour le mode d'adressage immédiat, parcours l'unité de calcul inchangée parce que NOP, et termine sur le bus d'adresse.
[[File:Chemin de données avec une ALU capable de faire des NOP.png|centre|vignette|upright=2|Chemin de données avec adressage immédiat étendu pour gérer des adresses.]]
Passons maintenant au cas particulier d'une instruction MOV qui copie une constante dans un registre. Il n'y a rien à faire si l'unité de calcul est capable d'effectuer une opération NOP/''pass through''. Pour charger une constante dans un registre, l'ALU est configurée pour faire un NOP, la constante traverse l'ALU et se retrouve dans les registres. Si l'ALU ne gère pas les NOP, la constante doit être envoyée sur l'entrée d'écriture du banc de registres, à travers un MUX dédié.
[[File:Implémentation de l'adressage immédiat dans le chemin de données.png|centre|vignette|upright=2|Implémentation de l'adressage immédiat dans le chemin de données]]
===Les architectures CISC : les opérations ''load-op''===
Tout ce qu'on a vu précédemment porte sur les processeurs de type LOAD-STORE, souvent confondus avec les processeurs de type RISC, où les accès mémoire sont séparés des instructions utilisant l'ALU. Il est maintenant temps de voir les processeurs CISC, qui gèrent des instructions ''load-op'', qui peuvent lire une opérande depuis la mémoire.
L'implémentation des opérations ''load-op'' relie le bus de donnée directement sur une entrée de l'unité de calcul, en utilisant encore une fois un multiplexeur. L'implémentation parait simple, mais c'est parce que toute la complexité est déportée dans le séquenceur. C'est lui qui se charge de détecter quand la lecture de l'opérande est terminée, quand l'opérande est disponible.
Les instructions ''load-op'' s'exécutent en plusieurs étapes, en plusieurs micro-opérations. Il y a typiquement une étape pour l'opérande à lire en mémoire et une étape de calcul. L'usage d'un registre d’interfaçage permet d'implémenter les instructions ''load-op'' très facilement. Une opération ''load-op'' charge l'opérande en mémoire dans un registre d’interfaçage, puis relier ce registre d’interfaçage sur une des entrées de l'ALU. Un simple multiplexeur suffit pour implémenter le tout, en plus des modifications adéquates du séquenceur.
[[File:Chemin de données d'un CPU CISC avec lecture des opérandes en mémoire.png|centre|vignette|upright=2|Chemin de données d'un CPU CISC avec lecture des opérandes en mémoire]]
Supporter les instructions multi-accès (qui font plusieurs accès mémoire) ne modifie pas fondamentalement le réseau d'interconnexion, ni le chemin de données La raison est que supporter les instructions multi-accès se fait au niveau du séquenceur. En réalité, les accès mémoire se font en série, l'un après l'autre, sous la commande du séquenceur qui émet plusieurs micro-opérations mémoire consécutives. Les données lues sont placées dans des registres d’interactivement mémoire, ce qui demande d'ajouter des registres d’interfaçage mémoire en plus.
==Annexe : le cas particulier du pointeur de pile==
Le pointeur de pile est un registre un peu particulier. Il peut être placé dans le chemin de données ou dans le séquenceur, voire dans l'unité de chargement, tout dépend du processeur. Tout dépend de si le pointeur de pile gère une pile d'adresses de retour ou une pile d'appel.
===Le pointeur de pile non-adressable explicitement===
Avec une pile d'adresse de retour, le pointeur de pile n'est pas adressable explicitement, il est juste adressé implicitement par des instructions d'appel de fonction CALL et des instructions de retour de fonction RET. Le pointeur de pile est alors juste incrémenté ou décrémenté par un pas constant, il ne subit pas d'autres opérations, son adressage est implicite. Il est juste incrémenté/décrémenté par pas constants, qui sont fournis par le séquenceur. Il n'y a pas besoin de le relier au chemin de données, vu qu'il n'échange pas de données avec les autres registres. Il y a alors plusieurs solutions, mais la plus simple est de placer le pointeur de pile dans le séquenceur et de l'incrémenter par un incrémenteur dédié.
Quelques processeurs simples disposent d'une pile d'appel très limitée, où le pointeur de pile n'est pas adressable explicitement. Il est adressé implicitement par les instruction CALL, RET, mais aussi PUSH et POP, mais aucune autre instruction ne permet cela. Là encore, le pointeur de pile ne communique pas avec les autres registres. Il est juste incrémenté/décrémenté par pas constants, qui sont fournis par le séquenceur. Là encore, le plus simple est de placer le pointeur de pile dans le séquenceur et de l'incrémenter par un incrémenteur dédié.
Dans les deux cas, le pointeur de pile est placé dans l'unité de contrôle, le séquenceur, et est associé à un incrémenteur dédié. Il se trouve que cet incrémenteur est souvent partagé avec le ''program counter''. En effet, les deux sont des adresses mémoire, qui sont incrémentées et décrémentées par pas constants, ne subissent pas d'autres opérations (si ce n'est des branchements, mais passons). Les ressemblances sont suffisantes pour fusionner les deux circuits. Ils peuvent donc avoir un '''incrémenteur partagé'''.
L'incrémenteur en question est donc partagé entre pointeur de pile, ''program counter'' et quelques autres registres similaires. Par exemple, le Z80 intégrait un registre pour le rafraichissement mémoire, qui était réalisé par le CPU à l'époque. Ce registre contenait la prochaine adresse mémoire à rafraichir, et était incrémenté à chaque rafraichissement d'une adresse. Et il était lui aussi intégré au séquenceur et incrémenté par l'incrémenteur partagé.
[[File:Organisation interne d'une architecture à pile.png|centre|vignette|upright=2|Organisation interne d'une architecture à pile]]
===Le pointeur de pile adressable explicitement===
Maintenant, étudions le cas d'une pile d'appel, précisément d'une pile d'appel avec des cadres de pile de taille variable. Sous ces conditions, le pointeur de pile est un registre adressable, avec un nom/numéro de registre dédié. Tel est par exemple le cas des processeurs x86 avec le registre ESP (''Extended Stack Pointer''). 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 calculs d'adresse, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres.
Dans ce cas, la meilleure solution est de placer le pointeur de pile dans le banc de registre généraux, avec les autres registres entiers. En faisant cela, la manipulation du pointeur de pile est faite par l'unité de calcul entière, pas besoin d'utiliser un incrémenteur dédiée. Il a existé des processeurs qui mettaient le pointeur de pile dans le banc de registre, mais l'incrémentaient avec un incrémenteur dédié, mais nous les verrons dans le chapitre sur les architectures à accumulateur. La raison est que sur les processeurs concernés, les adresses ne faisaient pas la même taille que les données : c'était des processeurs 8 bits, qui géraient des adresses de 16 bits.
==Annexe : l'implémentation du système d'''aliasing'' des registres des CPU x86==
Il y a quelques chapitres, nous avions parlé du système d'''aliasing'' des registres des CPU x86. Pour rappel, il permet de donner plusieurs noms de registre pour un même registre. Plus précisément, pour un registre 64 bits, le registre complet aura un nom de registre, les 32 bits de poids faible auront leur nom de registre dédié, idem pour les 16 bits de poids faible, etc. Il est possible de faire des calculs sur ces moitiés/quarts/huitièmes de registres sans problème.
===L'''aliasing'' du 8086, pour les registres 16 bits===
[[File:Register 8086.PNG|vignette|Register 8086]]
L'implémentation de l'''aliasing'' est apparue sur les premiers CPU Intel 16 bits, notamment le 8086. En tout, ils avaient quatre registres généraux 16 bits : AX, BX, CX et DX. Ces quatre registres 16 bits étaient coupés en deux octets, chacun adressable. Par exemple, le registre AX était coupé en deux octets nommés AH et AL, chacun ayant son propre nom/numéro de registre. Les instructions d'addition/soustraction pouvaient manipuler le registre AL, ou le registre AH, ce qui modifiait les 8 bits de poids faible ou fort selon le registre choisit.
Le banc de registre ne gére que 4 registres de 16 bits, à savoir AX, BX, CX et DX. Lors d'une lecture d'un registre 8 bits, le registre 16 bit entier est lu depuis le banc de registre, mais les bits inutiles sont ignorés. Par contre, l'écriture peut se faire soit avec 16 bits d'un coup, soit pour seulement un octet. Le port d'écriture du banc de registre peut être configuré de manière à autoriser l'écriture soit sur les 16 bits du registre, soit seulement sur les 8 bits de poids faible, soit écrire dans les 8 bits de poids fort.
[[File:Port d'écriture du banc de registre du 8086.png|centre|vignette|upright=2.5|Port d'écriture du banc de registre du 8086]]
Une opération sur un registre 8 bits se passe comme suit. Premièrement, on lit le registre 16 bits complet depuis le banc de registre. Si l'on a sélectionné l'octet de poids faible, il ne se passe rien de particulier, l'opérande 16 bits est envoyée directement à l'ALU. Mais si on a sélectionné l'octet de poids fort, la valeur lue est décalée de 7 rangs pour atterrir dans les 8 octets de poids faible. Ensuite, l'unité de calcul fait un calcul avec cet opérande, un calcul 16 bits tout ce qu'il y a de plus classique. Troisièmement, le résultat est enregistré dans le banc de registre, en le configurant convenablement. La configuration précise s'il faut enregistrer le résultat dans un registre 16 bits, soit seulement dans l'octet de poids faible/fort.
Afin de simplifier le câblage, les 16 bits des registres AX/BX/CX/DX sont entrelacés d'une manière un peu particulière. Intuitivement, on s'attend à ce que les bits soient physiquement dans le même ordre que dans le registre : le bit 0 est placé à côté du bit 1, suivi par le bit 2, etc. Mais à la place, l'octet de poids fort et de poids faible sont mélangés. Deux bits consécutifs appartiennent à deux octets différents. Le tout est décrit dans le tableau ci-dessous.
{|class="wikitable"
|-
! Registre 16 bits normal
| class="f_bleu" | 15
| class="f_bleu" | 14
| class="f_bleu" | 13
| class="f_bleu" | 12
| class="f_bleu" | 11
| class="f_bleu" | 10
| class="f_bleu" | 9
| class="f_bleu" | 8
| class="f_rouge" | 7
| class="f_rouge" | 6
| class="f_rouge" | 5
| class="f_rouge" | 4
| class="f_rouge" | 3
| class="f_rouge" | 2
| class="f_rouge" | 1
| class="f_rouge" | 0
|-
! Registre 16 bits du 8086
| class="f_bleu" | 15
| class="f_rouge" | 7
| class="f_bleu" | 14
| class="f_rouge" | 6
| class="f_bleu" | 13
| class="f_rouge" | 5
| class="f_bleu" | 12
| class="f_rouge" | 4
| class="f_bleu" | 11
| class="f_rouge" | 3
| class="f_bleu" | 10
| class="f_rouge" | 2
| class="f_bleu" | 9
| class="f_rouge" | 1
| class="f_bleu" | 8
| class="f_rouge" | 0
|}
En faisant cela, le décaleur en entrée de l'ALU est bien plus simple. Il y a 8 multiplexeurs, mais le câblage est bien plus simple. Par contre, en sortie de l'ALU, il faut remettre les bits du résultat dans l'ordre adéquat, celui du registre 8086. Pour cela, les interconnexions sur le port d'écriture sont conçues pour. Il faut juste mettre les fils de sortie de l'ALU sur la bonne entrée, par besoin de multiplexeurs.
===L'''aliasing'' sur les processeurs x86 32/64 bits===
Les processeurs x86 32 et 64 bits ont un système d'''aliasing'' qui complète le système précédent. Les processeurs 32 bits étendent les registres 16 bits existants à 32 bits. Pour ce faire, le registre 32 bit a un nouveau nom de registre, distincts du nom de registre utilisé pour l'ancien registre 16 bits. Il est possible d'adresser les 16 bits de poids faible de ce registre, avec le même nom de registre que celui utilisé pour le registre 16 sur les processeurs d'avant. Même chose avec les processeurs 64, avec l'ajout d'un nouveau nom de registre pour adresser un registre de 64 bit complet.
En soit, implémenter ce système n'est pas compliqué. Prenons le cas du registre RAX (64 bits), et de ses subdivisions nommées EAX (32 bits), AX (16 bits). À l'intérieur du banc de registre, il n'y a que le registre RAX. Le banc de registre ne comprend qu'un seul nom de registre : RAX. Les subdivisions EAX et AX n'existent qu'au niveau de l'écriture dans le banc de registre. L'écriture dans le banc de registre est configurable, de manière à ne modifier que les bits adéquats. Le résultat d'un calcul de l'ALU fait 64 bits, il est envoyé sur le port d'écriture. À ce niveau, soit les 64 bits sont écrits dans le registre, soit seulement les 32/16 bits de poids faible. Le système du 8086 est préservé pour les écritures dans les 16 bits de poids faible.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les composants d'un processeur
| prevText=Les composants d'un processeur
| next=L'unité de chargement et le program counter
| nextText=L'unité de chargement et le program counter
}}
</noinclude>
822y18pig8gme0vlp0ewgfdfje6q0hl
745833
745816
2025-07-02T20:38:37Z
Mewtow
31375
/* Les unités de calcul spécialisées */
745833
wikitext
text/x-wiki
Comme vu précédemment, le '''chemin de donnée''' est l'ensemble des composants dans lesquels circulent les données dans le processeur. Il comprend l'unité de calcul, les registres, l'unité de communication avec la mémoire, et le ou les interconnexions qui permettent à tout ce petit monde de communiquer. Dans ce chapitre, nous allons voir ces composants en détail.
==Les unités de calcul==
Le processeur contient des circuits capables de faire des calculs arithmétiques, des opérations logiques, et des comparaisons, qui sont regroupés dans une unité de calcul appelée '''unité arithmétique et logique'''. Certains préfèrent l’appellation anglaise ''arithmetic and logic unit'', ou ALU. Par défaut, ce terme est réservé aux unités de calcul qui manipulent des nombres entiers. Les unités de calcul spécialisées pour les calculs flottants sont désignées par le terme "unité de calcul flottant", ou encore FPU (''Floating Point Unit'').
L'interface d'une unité de calcul est assez simple : on a des entrées pour les opérandes et une sortie pour le résultat du calcul. De plus, les instructions de comparaisons ou de calcul peuvent mettre à jour le registre d'état, qui est relié à une autre sortie de l’unité de calcul. Une autre entrée, l''''entrée de sélection de l'instruction''', spécifie l'opération à effectuer. Elle sert à configurer l'unité de calcul pour faire une addition et pas une multiplication, par exemple. Sur cette entrée, on envoie un numéro qui précise l'opération à effectuer. La correspondance entre ce numéro et l'opération à exécuter dépend de l'unité de calcul. Sur les processeurs où l'encodage des instructions est "simple", une partie de l'opcode de l'instruction est envoyé sur cette entrée.
[[File:Unité de calcul usuelle.png|centre|vignette|upright=2|Unité de calcul usuelle.]]
Il faut signaler que les processeurs modernes possèdent plusieurs unités de calcul, toutes reliées aux registres. Cela permet d’exécuter plusieurs calculs en même temps dans des unités de calcul différentes, afin d'augmenter les performances du processeur. Diverses technologies, abordées dans la suite du cours permettent de profiter au mieux de ces unités de calcul : pipeline, exécution dans le désordre, exécution superscalaire, jeux d'instructions VLIW, etc. Mais laissons cela de côté pour le moment.
===L'ALU entière : additions, soustractions, opérations bit à bit===
Un processeur contient plusieurs ALUs spécialisées. La principale, présente sur tous les processeurs, est l''''ALU entière'''. Elle s'occupe uniquement des opérations sur des nombres entiers, les nombres flottants sont gérés par une ALU à part. Elle gère des opérations simples : additions, soustractions, opérations bit à bit, parfois des décalages/rotations. Par contre, elle ne gère pas la multiplication et la division, qui sont prises en charge par un circuit multiplieur/diviseur à part.
L'ALU entière a déjà été vue dans un chapitre antérieur, nommé "Les unités arithmétiques et logiques entières (simples)", qui expliquait comment en concevoir une. Nous avions vu qu'une ALU entière est une sorte de circuit additionneur-soustracteur amélioré, ce qui explique qu'elle gère des opérations entières simples, mais pas la multiplication ni la division. Nous ne reviendrons pas dessus. Cependant, il y a des choses à dire sur leur intégration au processeur.
Une ALU entière gère souvent une opération particulière, qui ne fait rien et recopie simplement une de ses opérandes sur sa sortie. L'opération en question est appelée l''''opération ''Pass through''''', encore appelée opération NOP. Elle est implémentée en utilisant un simple multiplexeur, placé en sortie de l'ALU. Le fait qu'une ALU puisse effectuer une opération ''Pass through'' permet de fortement simplifier le chemin de donnée, d'économiser des multiplexeurs. Mais nous verrons cela sous peu.
[[File:ALU avec opération NOP.png|centre|vignette|upright=2|ALU avec opération NOP.]]
Avant l'invention du microprocesseur, le processeur n'était pas un circuit intégré unique. L'ALU, le séquenceur et les registres étaient dans des puces séparées. Les ALU étaient vendues séparément et manipulaient des opérandes de 4/8 bits, les ALU 4 bits étaient très fréquentes. Si on voulait créer une ALU pour des opérandes plus grandes, il fallait construire l'ALU en combinant plusieurs ALU 4/8 bits. Par exemple, l'ALU des processeurs AMD Am2900 est une ALU de 16 bits composée de plusieurs sous-ALU de 4 bits. Cette technique qui consiste à créer des unités de calcul à partir d'unités de calcul plus élémentaires s'appelle en jargon technique du '''bit slicing'''. Nous en avions parlé dans le chapitre sur les unités de calcul, aussi nous n'en reparlerons pas plus ici.
L'ALU manipule des opérandes codées sur un certain nombre de bits. Par exemple, une ALU peut manipuler des entiers codés sur 8 bits, sur 16 bits, etc. En général, la taille des opérandes de l'ALU est la même que la taille des registres. Un processeur 32 bits, avec des registres de 32 bit, a une ALU de 32 bits. C'est intuitif, et cela rend l'implémentation du processeur bien plus facile. Mais il y a quelques exceptions, où l'ALU manipule des opérandes plus petits que la taille des registres. Par exemple, de nombreux processeurs 16 bits, avec des registres de 16 bits, utilisent une ALU de 8 bits. Un autre exemple assez connu est celui du Motorola 68000, qui était un processeur 32 bits, mais dont l'ALU faisait juste 16 bits. Son successeur, le 68020, avait lui une ALU de 32 bits.
Sur de tels processeurs, les calculs sont fait en plusieurs passes. Par exemple, avec une ALU 8 bit, les opérations sur des opérandes 8 bits se font en un cycle d'horloge, celles sur 16 bits se font en deux cycles, celles en 32 en quatre, etc. Si un programme manipule assez peu d'opérandes 16/32/64 bits, la perte de performance est assez faible. Diverses techniques visent à améliorer les performances, mais elles ne font pas de miracles. Par exemple, vu que l'ALU est plus courte, il est possible de la faire fonctionner à plus haute fréquence, pour réduire la perte de performance.
Pour comprendre comme est implémenté ce système de passes, prenons l'exemple du processeur 8 bit Z80. Ses registres entiers étaient des registres de 8 bits, alors que l'ALU était de 4 bits. Les calculs étaient faits en deux phases : une qui traite les 4 bits de poids faible, une autre qui traite les 4 bits de poids fort. Pour cela, les opérandes étaient placées dans des registres de 4 bits en entrée de l'ALU, plusieurs multiplexeurs sélectionnaient les 4 bits adéquats, le résultat était mémorisé dans un registre de résultat de 8 bits, un démultiplexeur plaçait les 4 bits du résultat au bon endroit dans ce registre. L'unité de contrôle s'occupait de la commande des multiplexeurs/démultiplexeurs. Les autres processeurs 8 ou 16 bits utilisent des circuits similaires pour faire leurs calculs en plusieurs fois.
[[File:ALU du Z80.png|centre|vignette|upright=2|ALU du Z80]]
Un exemple extrême est celui des des '''processeurs sériels''' (sous-entendu ''bit-sériels''), qui utilisent une '''ALU sérielle''', qui fait leurs calculs bit par bit, un bit à la fois. S'il a existé des processeurs de 1 bit, comme le Motorola MC14500B, la majeure partie des processeurs sériels étaient des processeurs 4, 8 ou 16 bits. L'avantage de ces ALU est qu'elles utilisent peu de transistors, au détriment des performances par rapport aux processeurs non-sériels. Mais un autre avantage est qu'elles peuvent gérer des opérandes de grande taille, avec plus d'une trentaine de bits, sans trop de problèmes.
===Les circuits multiplieurs et diviseurs===
Les processeurs modernes ont une ALU pour les opérations simples (additions, décalages, opérations logiques), couplée à une ALU pour les multiplications, un circuit multiplieur séparé. Précisons qu'il ne sert pas à grand chose de fusionner le circuit multiplieur avec l'ALU, mieux vaut les garder séparés par simplicité. Les processeurs haute performance disposent systématiquement d'un circuit multiplieur et gèrent la multiplication dans leur jeu d'instruction.
Le cas de la division est plus compliqué. La présence d'un circuit multiplieur est commune, mais les circuits diviseurs sont eux très rares. Leur cout en circuit est globalement le même que pour un circuit multiplieur, mais le gain en performance est plus faible. Le gain en performance pour la multiplication est modéré car il s'agit d'une opération très fréquente, alors qu'il est très faible pour la division car celle-ci est beaucoup moins fréquente.
Pour réduire le cout en circuits, il arrive que l'ALU pour les multiplications gère à la fois la multiplication et la division. Les circuits multiplieurs et diviseurs sont en effet très similaires et partagent beaucoup de points communs. Généralement, la fusion se fait pour les multiplieurs/diviseurs itératifs.
Il existe cependant des circuits qui se passent de multiplieurs, tout en supportant la multiplication dans leur jeu d'instruction. Certains utilisent pour cela du microcode, technique qu'on verra dans deux chapitres, mais l'Intel Atom utilise une technique franchement peu ordinaire. L'Intel Atom utilise l'unité de calcul flottante pour faire les multiplications entières. Les opérandes entières sont traduites en nombres flottants, multipliés par l'unité de calcul flottante, puis le résultat est converti en un entier avec quelques corrections à la clé. Ainsi, on fait des économies de circuits, en mutualisant le multiplieur entre l'unité de calcul flottante et l'ALU entière, surtout que ce multiplieur manipule des opérandes plus courtes. Les performances sont cependant réduites comparé à l'usage d'un vrai multiplieur entier.
===Le ''barrel shifter''===
On vient d'expliquer que la présence de plusieurs ALU spécialisée est très utile pour implémenter des opérations compliquées à insérer dans une unité de calcul normale, comme la multiplication et la division. Mais les décalages sont aussi dans ce cas, de même que les rotations. Nous avions vu il y a quelques chapitres qu'ils sont réalisés par un circuit spécialisé, appelé un ''barrel shifter'', qu'il est difficile de fusionner avec une ALU normale. Aussi, beaucoup de processeurs incorporent un ''barrel shifter'' séparé de l'ALU.
Les processeurs ARM utilise un ''barrel shifter'', mais d'une manière un peu spéciale. On a vu il y a quelques chapitres que si on fait une opération logique, une addition, une soustraction ou une comparaison, la seconde opérande peut être décalée automatiquement. L'instruction incorpore le type de de décalage à faire et par combien de rangs il faut décaler directement à côté de l'opcode. Cela simplifie grandement les calculs d'adresse, qui se font en une seule instruction, contre deux ou trois sur d'autres architectures. Et pour cela, l'ALU proprement dite est précédée par un ''barrel shifter'',une seconde ALU spécialisée dans les décalages. Notons que les instructions MOV font aussi partie des instructions où la seconde opérande (le registre source) peut être décalé : cela signifie que les MOV passent par l'ALU, qui effectue alors un NOP, une opération logique OUI.
===Les unités de calcul spécialisées===
Un processeur peut disposer d’unités de calcul séparées de l'unité de calcul principale, spécialisées dans les décalages, les divisions, etc. Et certaines d'entre elles sont spécialisées dans des opérations spécifiques, qui ne sont techniquement pas des opérations entières, sur des nombres entiers.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Depuis les années 90-2000, presque tous les processeurs utilisent une unité de calcul spécialisée pour les nombres flottants : la '''Floating-Point Unit''', aussi appelée FPU. En général, elle regroupe un additionneur-soustracteur flottant et un multiplieur flottant. Parfois, elle incorpore un diviseur flottant, tout dépend du processeur. Précisons que sur certains processeurs, la FPU et l'ALU entière ne vont pas à la même fréquence, pour des raisons de performance et de consommation d'énergie !
La FPU intègre un circuit multiplieur entier, qui pour les multiplications flottantes, afin de multiplier les mantisses entre elles. Quelques processeurs utilisaient ce multiplier pour faire les multiplications entières. En clair, au lieu d'avoir un multiplieur entier séparé du multiplier flottant, les deux sont fusionnés en un seul circuit. Il s'agit d'une optimisation qui a été utilisée sur quelques processeurs 32 bits, qui supportaient les flottants 64 bits (double précision). Les processeurs Atom étaient dans ce cas, idem pour l'Athlon première génération. Les processeurs modernes n'utilisent pas cette optimisation pour des raisons qu'on ne peut pas expliquer ici (réduction des dépendances structurelles, émission multiple).
Il existe des unités de calcul spécialisées pour les calculs d'adresse. Elles ne supportent guère plus que des incrémentations/décrémentations, des additions/soustractions, et des décalages simples. Les autres opérations n'ont pas de sens avec des adresses. L'usage d'ALU spécialisées pour les adresses est un avantage sur les processeurs où les adresses ont une taille différente des données, ce qui est fréquent sur les anciens processeurs 8 bits.
De nombreux processeurs modernes disposent d'une unité de calcul spécialisée dans le calcul des conditions, tests et branchements. C’est notamment le cas sur les processeurs sans registre d'état, qui disposent de registres à prédicats. En général, les registres à prédicats sont placés à part des autres registres, dans un banc de registre séparé. L'unité de calcul normale n'est pas reliée aux registres à prédicats, alors que l'unité de calcul pour les branchements/test/conditions l'est. les registres à prédicats sont situés juste en sortie de cette unité de calcul.
==Les registres du processeur==
Après avoir vu l'unité de calcul, il est temps de passer aux registres d'un processeur. L'organisation des registres est généralement assez compliquée, avec quelques registres séparés des autres comme le registre d'état ou le ''program counter''. Les registres d'un processeur peuvent se classer en deux camps : soit ce sont des registres isolés, soit ils sont regroupés en paquets appelés banc de registres.
Un '''banc de registres''' (''register file'') est une RAM, dont chaque byte est un registre. Il regroupe un paquet de registres différents dans un seul composant, dans une seule mémoire. Dans processeur moderne, on trouve un ou plusieurs bancs de registres. La répartition des registres, à savoir quels registres sont dans le banc de registre et quels sont ceux isolés, est très variable suivant les processeurs.
[[File:Register File Simple.svg|centre|vignette|upright=1|Banc de registres simplifié.]]
===L'adressage du banc de registres===
Le banc de registre est une mémoire comme une autre, avec une entrée d'adresse qui permet de sélectionner le registre voulu. Plutot que d'adresse, nous allons parler d''''identifiant de registre'''. Le séquenceur forge l'identifiant de registre en fonction des registres sélectionnés. Dans les chapitres précédents, nous avions vu qu'il existe plusieurs méthodes pour sélectionner un registre, qui portent les noms de modes d'adressage. Et bien les modes d'adressage jouent un grand rôle dans la forge de l'identifiant de registre.
Pour rappel, sur la quasi-totalité des processeurs actuels, les registres généraux sont identifiés par un nom de registre, terme trompeur vu que ce nom est en réalité un numéro. En clair, les processeurs numérotent les registres, le numéro/nom du registre permettant de l'identifier. Par exemple, si je veux faire une addition, je dois préciser les deux registres pour les opérandes, et éventuellement le registre pour le résultat : et bien ces registres seront identifiés par un numéro. Mais tous les registres ne sont pas numérotés et ceux qui ne le sont pas sont adressés implicitement. Par exemple, le pointeur de pile sera modifié par les instructions qui manipulent la pile, sans que cela aie besoin d'être précisé par un nom de registre dans l'instruction.
Dans le cas le plus simple, les registres nommés vont dans le banc de registres, les registres adressés implicitement sont en-dehors, dans des registres isolés. L'idéntifiant de registre est alors simplement le nom de registre, le numéro. Le séquenceur extrait ce nom de registre de l'insutrction, avant de l'envoyer sur l'entrée d'adresse du banc de registre.
[[File:Adressage du banc de registres généruax.png|centre|vignette|upright=2|Adressage du banc de registres généraux]]
Dans un cas plus complexe, des registres non-nommés sont placés dans le banc de registres. Par exemple, les pointeurs de pile sont souvent placés dans le banc de registre, même s'ils sont adressés implicitement. Même des registres aussi importants que le ''program counter'' peuvent se mettre dans le banc de registre ! Nous verrons le cas du ''program counter'' dans le chapitre suivant, qui porte sur l'unité de chargement. Dans ce cas, le séquenceur forge l'identifiant de registre de lui-même. Dans le cas des registres nommés, il ajoute quelques bits aux noms de registres. Pour les registres adressés implicitement, il forge l'identifiant à partir de rien.
[[File:Adressage du banc de registre - cas général.png|centre|vignette|upright=2|Adressage du banc de registre - cas général]]
Nous verrons plus bas que dans certains cas, le nom de registre ne suffit pas à adresser un registre dans un banc de registre. Dans ce cas, le séquenceur rajoute des bits, comme dans l'exemple précédent. Tout ce qu'il faut retenir est que l'identifiant de registre est forgé par le séquenceur, qui se base entre autres sur le nom de registre s'il est présent, sur l'instruction exécutée dans le cas d'un registre adressé implicitement.
===Les registres généraux===
Pour rappel, les registres généraux peuvent mémoriser des entiers, des adresses, ou toute autre donnée codée en binaire. Ils sont souvent séparés des registres flottants sur les architectures modernes. Les registres généraux sont rassemblés dans un banc de registre dédié, appelé le '''banc de registres généraux'''. Le banc de registres généraux est une mémoire multiport, avec au moins un port d'écriture et deux ports de lecture. La raison est que les instructions lisent deux opérandes dans les registres et enregistrent leur résultat dans des registres. Le tout se marie bien avec un banc de registre à deux de lecture (pour les opérandes) et un d'écriture (pour le résultat).
[[File:Banc de registre multiports.png|centre|vignette|upright=2|Banc de registre multiports.]]
L'interface exacte dépend de si l'architecture est une architecture 2 ou 3 adresses. Pour rappel, la différence entre les deux tient dans la manière dont on précise le registre où enregistrer le résultat d'une opération. Avec les architectures 2-adresses, on précise deux registres : le premier sert à la fois comme opérande et pour mémoriser le résultat, l'autre sert uniquement d'opérande. Un des registres est donc écrasé pour enregistrer le résultat. Sur les architecture 3-adresses, on précise trois registres : deux pour les opérandes, un pour le résultat.
Les architectures 2-adresses ont un banc de registre où on doit préciser deux "adresses", deux noms de registre. L'interface du banc de registre est donc la suivante :
[[File:Register File Medium.svg|centre|vignette|upright=1.5|Register File d'une architecture à 2-adresses]]
Les architectures 3-adresses doivent rajouter une troisième entrée pour préciser un troisième nom de registre. L'interface du banc de registre est donc la suivante :
[[File:Register File Large.svg|centre|vignette|upright=1.5|Register File d'une architecture à 3-adresses]]
Rien n'empêche d'utiliser plusieurs bancs de registres sur un processeur qui utilise des registres généraux. La raison est une question d'optimisation. Au-delà d'un certain nombre de registres, il devient difficile d'utiliser un seul gros banc de registres. Il faut alors scinder le banc de registres en plusieurs bancs de registres séparés. Le problème est qu'il faut prévoir de quoi échanger des données entre les bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue du langage machine. Sur d'autres processeurs, les transferts de données entre bancs de registres se font via une instruction spéciale, souvent appelée COPY.
===Les registres flottants : banc de registre séparé ou unifié===
Passons maintenant aux registres flottants. Intuitivement, on a des registres séparés pour les entiers et les flottants. Il est alors plus simple d'utiliser un banc de registres séparé pour les nombres flottants, à côté d'un banc de registre entiers. L'avantage est que les nombres flottants et entiers n'ont pas forcément la même taille, ce qui se marie bien avec deux bancs de registres, où la taille des registres est différente dans les deux bancs.
Mais d'autres processeurs utilisent un seul '''banc de registres unifié''', qui regroupe tous les registres de données, qu'ils soient entier ou flottants. Par exemple, c'est le cas des Pentium Pro, Pentium II, Pentium III, ou des Pentium M : ces processeurs ont des registres séparés pour les flottants et les entiers, mais ils sont regroupés dans un seul banc de registres. Avec cette organisation, un registre flottant et un registre entier peuvent avoir le même nom de registre en langage machine, mais l'adresse envoyée au banc de registres ne doit pas être la même : le séquenceur ajoute des bits au nom de registre pour former l'adresse finale.
[[File:Désambiguïsation de registres sur un banc de registres unifié.png|centre|vignette|upright=2|Désambiguïsation de registres sur un banc de registres unifié.]]
===Le registre d'état===
Le registre d'état fait souvent bande à part et n'est pas placé dans un banc de registres. En effet, le registre d'état est très lié à l'unité de calcul. Il reçoit des indicateurs/''flags'' provenant de la sortie de l'unité de calcul, et met ceux-ci à disposition du reste du processeur. Son entrée est connectée à l'unité de calcul, sa sortie est reliée au séquenceur et/ou au bus interne au processeur.
Le registre d'état est relié au séquenceur afin que celui-ci puisse gérer les instructions de branchement, qui ont parfois besoin de connaitre certains bits du registre d'état pour savoir si une condition a été remplie ou non. D'autres processeurs relient aussi le registre d'état au bus interne, ce qui permet de lire son contenu et de le copier dans un registre de données. Cela permet d'implémenter certaines instructions, notamment celles qui permettent de mémoriser le registre d'état dans un registre général.
[[File:Place du registre d'état dans le chemin de données.png|centre|vignette|upright=2|Place du registre d'état dans le chemin de données]]
L'ALU fournit une sortie différente pour chaque bit du registre d'état, la connexion du registre d'état est directe, comme indiqué dans le schéma suivant. Vous remarquerez que le bit de retenue est à la fois connecté à la sortie de l'ALU, mais aussi sur son entrée. Ainsi, le bit de retenue calculé par une opération peut être utilisé pour la suivante. Sans cela, diverses instructions comme les opérations ''add with carry'' ne seraient pas possibles.
[[File:AluStatusRegister.svg|centre|vignette|upright=2|Registre d'état et unit de calcul.]]
Il est techniquement possible de mettre le registre d'état dans le banc de registre, pour économiser un registre. La principale difficulté est que les instructions doivent faire deux écritures dans le banc de registre : une pour le registre de destination, une pour le registre d'état. Soit on utilise deux ports d'écriture, soit on fait les deux écritures l'une après l'autre. Dans les deux cas, le cout en performances et en transistors n'en vaut pas le cout. D'ailleurs, je ne connais aucun processeur qui utilise cette technique.
Il faut noter que le registre d'état n'existe pas forcément en tant que tel dans le processeur. Quelques processeurs, dont le 8086 d'Intel, utilisent des bascules dispersées dans le processeur au lieu d'un vrai registre d'état. Les bascules dispersées mémorisent chacune un bit du registre d'état et sont placées là où elles sont le plus utile. Les bascules utilisées pour les branchements sont proches du séquenceur, le bascules pour les bits de retenue sont placées proche de l'ALU, etc.
===Les registres à prédicats===
Les registres à prédicats remplacent le registre d'état sur certains processeurs. Pour rappel, les registres à prédicat sont des registres de 1 bit qui mémorisent les résultats des comparaisons et instructions de test. Ils sont nommés/numérotés, mais les numéros en question sont distincts de ceux utilisés pour les registres généraux.
Ils sont placés à part, dans un banc de registres séparé. Le banc de registres à prédicats a une entrée de 1 bit connectée à l'ALU et une sortie de un bit connectée au séquenceur. Le banc de registres à prédicats est parfois relié à une unité de calcul spécialisée dans les conditions/instructions de test. Pour rappel, certaines instructions permettent de faire un ET, un OU, un XOR entre deux registres à prédicats. Pour cela, l'unité de calcul dédiée aux conditions peut lire les registres à prédicats, pour combiner le contenu de plusieurs d'entre eux.
[[File:Banc de registre pour les registres à prédicats.png|centre|vignette|upright=2|Banc de registre pour les registres à prédicats]]
===Les registres dédiés aux interruptions===
Dans le chapitre sur les registres, nous avions vu que certains processeurs dupliquaient leurs registres architecturaux, pour accélérer les interruptions ou les appels de fonction. Dans le cas qui va nous intéresser, les interruptions avaient accès à leurs propres registres, séparés des registres architecturaux. Les processeurs de ce type ont deux ensembles de registres identiques : un dédié aux interruptions, un autre pour les programmes normaux. Les registres dans les deux ensembles ont les mêmes noms, mais le processeur choisit le bon ensemble suivant s'il est dans une interruption ou non. Si on peut utiliser deux bancs de registres séparés, il est aussi possible d'utiliser un banc de registre unifié pour les deux.
Sur certains processeurs, le banc de registre est dupliqué en plusieurs exemplaires. La technique est utilisée pour les interruptions. Certains processeurs ont deux ensembles de registres identiques : un dédié aux interruptions, un autre pour les programmes normaux. Les registres dans les deux ensembles ont les mêmes noms, mais le processeur choisit le bon ensemble suivant s'il est dans une interruption ou non. On peut utiliser deux bancs de registres séparés, un pour les interruptions, et un pour les programmes.
Sur d'autres processeurs, on utilise un banc de registre unifié pour les deux ensembles de registres. Les registres pour les interruptions sont dans les adresses hautes, les registres pour les programmes dans les adresses basses. Le choix entre les deux est réalisé par un bit qui indique si on est dans une interruption ou non, disponible dans une bascule du processeur. Appelons là la bascule I.
===Le fenêtrage de registres===
[[File:Fenetre de registres.png|vignette|upright=1|Fenêtre de registres.]]
Le '''fenêtrage de registres''' fait que chaque fonction a accès à son propre ensemble de registres, sa propre fenêtre de registres. Là encore, cette technique duplique chaque registre architectural en plusieurs exemplaires qui portent le même nom. Chaque ensemble de registres architecturaux forme une fenêtre de registre, qui contient autant de registres qu'il y a de registres architecturaux. Lorsqu'une fonction s’exécute, elle se réserve une fenêtre inutilisée, et peut utiliser les registres de la fenêtre comme bon lui semble : une fonction manipule le registre architectural de la fenêtre réservée, mais pas les registres avec le même nom dans les autres fenêtres.
Il peut s'implémenter soit avec un banc de registres unifié, soit avec un banc de registre par fenêtre de registres.
Il est possible d'utiliser des bancs de registres dupliqués pour le fenêtrage de registres. Chaque fenêtre de registre a son propre banc de registres. Le choix entre le banc de registre à utiliser est fait par un registre qui mémorise le numéro de la fenêtre en cours. Ce registre commande un multiplexeur qui permet de choisir le banc de registre adéquat.
[[File:Fenêtrage de registres au niveau du banc de registres.png|vignette|Fenêtrage de registres au niveau du banc de registres.]]
L'utilisation d'un banc de registres unifié permet d'implémenter facilement le fenêtrage de registres. Il suffit pour cela de regrouper tous les registres des différentes fenêtres dans un seul banc de registres. Il suffit de faire comme vu au-dessus : rajouter des bits au nom de registre pour faire la différence entre les fenêtres. Cela implique de se souvenir dans quelle fenêtre de registre on est actuellement, cette information étant mémorisée dans un registre qui stocke le numéro de la fenêtre courante. Pour changer de fenêtre, il suffit de modifier le contenu de ce registre lors d'un appel ou retour de fonction avec un petit circuit combinatoire. Bien sûr, il faut aussi prendre en compte le cas où ce registre déborde, ce qui demande d'ajouter des circuits pour gérer la situation.
[[File:Désambiguïsation des fenêtres de registres.png|centre|vignette|upright=2|Désambiguïsation des fenêtres de registres.]]
==L'interface de communication avec la mémoire==
L''''interface avec la mémoire''' est, comme son nom l'indique, des circuits qui servent d'intermédiaire entre le bus mémoire et le processeur. Elle est parfois appelée l'unité mémoire, l'unité d'accès mémoire, la ''load-store unit'', et j'en oublie.
[[File:Unité de communication avec la mémoire, de type simple port.png|centre|vignette|upright=2|Unité de communication avec la mémoire, de type simple port.]]
Sur certains processeurs, elle gère les mémoires multiport.
[[File:Unité de communication avec la mémoire, de type multiport.png|centre|vignette|upright=2|Unité de communication avec la mémoire, de type multiport.]]
===Les registres d'interfaçage mémoire===
L'interface mémoire se résume le plus souvent à des '''registres d’interfaçage mémoire''', intercalés entre le bus mémoire et le chemin de données. Généralement, il y a au moins deux registres d’interfaçage mémoire : un registre relié au bus d'adresse, et autre relié au bus de données.
[[File:Registres d’interfaçage mémoire.png|centre|vignette|upright=2|Registres d’interfaçage mémoire.]]
Au lieu de lire ou écrire directement sur le bus, le processeur lit ou écrit dans ces registres, alors que l'unité d'accès mémoire s'occupe des échanges entre registres et bus mémoire. Lors d'une écriture, le processeur place l'adresse dans le registre d'interfaçage d'adresse, met la donnée à écrire dans le registre d'interfaçage de donnée, puis laisse l'unité d'accès mémoire faire son travail. Lors d'une lecture, il place l'adresse à lire sur le registre d'interfaçage d'adresse, il attend que la donnée soit lue, puis récupère la donnée dans le registre d'interfaçage de données.
L'avantage est que le processeur n'a pas à maintenir une donnée/adresse sur le bus durant tout un accès mémoire. Par exemple, prenons le cas où la mémoire met 15 cycles processeurs pour faire une lecture ou une écriture. Sans registres d'interfaçage mémoire, le processeur doit maintenir l'adresse durant 15 cycles, et aussi la donnée dans le cas d'une écriture. Avec ces registres, le processeur écrit dans les registres d'interfaçage mémoire au premier cycle, et passe les 14 cycles suivants à faire quelque chose d'autre. Par exemple, il faut faire un calcul en parallèle, envoyer des signaux de commande au banc de registre pour qu'il soit prêt une fois la donnée lue arrivée, etc. Cet avantage simplifie l'implémentation de certains modes d'adressage, comme on le verra à la fin du chapitre.
===L'unité de calcul d'adresse===
Les registres d'interfaçage sont presque toujours présents, mais le circuit que nous allons voir est complétement facultatif. Il s'agit d'une unité de calcul spécialisée dans les calculs d'adresse, dont nous avons parlé rapidement dans la section sur les ALU. Elle s'appelle l''''''Address generation unit''''', ou AGU. Elle est parfois séparée de l'interface mémoire proprement dit, et est alors considérée comme une unité de calcul à part, mais elle est généralement intimement liée à l'interface mémoire.
Elle sert pour certains modes d'adressage, qui demandent de combiner une adresse avec soit un indice, soit un décalage, plus rarement les deux. Les calculs d'adresse demandent de simplement incrémenter/décrémenter une adresse, de lui ajouter un indice (et de décaler les indices dans certains cas), mais guère plus. Pas besoin d'effectuer de multiplications, de divisions, ou d'autre opération plus complexe. Des décalages et des additions/soustractions suffisent. L'AGU est donc beaucoup plus simple qu'une ALU normale et se résume souvent à un vulgaire additionneur-soustracteur, éventuellement couplée à un décaleur pour multiplier les indices.
[[File:Unité d'accès mémoire avec unité de calcul dédiée.png|centre|vignette|upright=1.5|Unité d'accès mémoire avec unité de calcul dédiée]]
Le fait d'avoir une unité de calcul séparée pour les adresses peut s'expliquer pour plusieurs raisons. Sur les processeurs normaux, la raison est que cela simplifie un peu l'implémentation des modes d'adressage indirects. Sur les rares processeurs qui ont des registres séparés pour les adresses, un banc de registre dédié est réservé aux registres d'adresses, ce qui rend l'usage d'une unité de calcul d'adresse bien plus pratique. Une autre raison se manifestait sur les processeurs 8 bits : ils géraient des données de 8 bits, mais des adresses de 16 bits. Dans ce cas, le processeur avait une ALU simple de 16 bits pour les adresses, et une ALU complexe de 8 bits pour les données.
[[File:Unité d'accès mémoire avec registres d'adresse ou d'indice.png|centre|vignette|upright=2|Unité d'accès mémoire avec registres d'adresse ou d'indice]]
===La gestion de l'alignement et du boutisme===
L'interface mémoire gère les accès mémoire non-alignés, à cheval sur deux mots mémoire (rappelez-vous le chapitre sur l'alignement mémoire). Elle détecte les accès mémoire non-alignés et réagit en conséquence. Dans le cas où les accès non-alignés sont interdits, elle lève une exception matérielle. Dans le cas où ils sont autorisés, elle les gère automatiquement, à savoir qu'elle charge deux mots mémoire et les combine entre eux pour donner le résultat final. Dans les deux cas, cela demande d'ajouter des circuits de détection des accès non-alignés, et éventuellement des circuits pour le double lecture/écriture.
Les circuits de détection des accès non-alignés sont très simples. Dans le cas où les adresses sont alignées sur une puissance de deux (cas le plus courant), il suffit de vérifier les bits de poids faible de l'adresse à lire. Prenons l'exemple d'un processeur avec des adresses codées sur 64 bits, avec des mots mémoire de 32 bits, alignés sur 32 bits (4 octets). Un mot mémoire contient 4 octets, les contraintes d'alignement font que les adresses autorisées sont des multiples de 4. En conséquence, les 2 bits de poids faible d'une adresse valide sont censés être à 0. En vérifiant la valeur de ces deux bits, on détecte facilement les accès non-alignés.
En clair, détecter les accès non-alignés demande de tester si les bits de poids faibles adéquats sont à 0. Il suffit donc d'un circuit de comparaison avec zéro; qui est une simple porte OU. Cette porte OU génère un bit qui indique si l'accès testé est aligné ou non : 1 si l'accès est non-aligné, 0 sinon. Le signal peut être transmis au séquenceur pour générer une exception matérielle, ou utilisé dans l'unité d'accès mémoire pour la double lecture/écriture.
La gestion automatique des accès non-alignés est plus complexe. Dans ce cas, l'unité mémoire charge deux mots mémoire et les combine entre eux pour donner le résultat final. Charger deux mots mémoires consécutifs est assez simple, si le registre d'interfaçage est un compteur. L'accès initial charge le premier mot mémoire, puis l'adresse stockée dans le registre d'interfaçage est incrémentée pour démarrer un second accès. Le circuit pour combiner deux mots mémoire contient des registres, des circuits de décalage, des multiplexeurs.
===Le rafraichissement mémoire optimisé et le contrôleur mémoire intégré===
Depuis les années 80, les processeurs sont souvent combinés avec une mémoire principale de type DRAM. De telles mémoires doivent être rafraichies régulièrement pour ne pas perdre de données. Le rafraichissement se fait généralement adresse par adresse, ou ligne par ligne (les lignes sont des super-bytes internes à la DRAM). Le rafraichissement est en théorie géré par le contrôleur mémoire installé sur la carte mère. Mais au tout début de l'informatique, du temps des processeurs 8 bits, le rafraichissement mémoire était géré directement par le processeur.
Si quelques processeurs géraient le rafraichissement mémoire avec des interruptions, d'autres processeurs disposaient d’optimisations pour optimiser le rafraichissement mémoire. Divers processeurs implémentaient de quoi faciliter le rafraichissement par adresse. Par exemple, le processeur Zilog Z80 contenait un compteur de ligne, un registre qui contenait le numéro de la prochaine ligne à rafraichir. Il était incrémenté à chaque rafraichissement mémoire, automatiquement, par le processeur lui-même. Un ''timer'' interne permettait de savoir quand rafraichir la mémoire : quand ce ''timer'' atteignait 0, une commande de rafraichissement était envoyée à la mémoire, et le ''timer'' était ''reset''. Et tout cela était intégré à l'unité d'accès mémoire.
Depuis les années 2000, les processeurs modernes ont un contrôleur mémoire DRAM intégré directement dans le processeur. Ce qui fait qu'ils gèrent non seulement le rafraichissement, mais aussi d'autres fonctions bien pus complexes.
==Le chemin de données et son réseau d'interconnexions==
Nous venons de voir que le chemin de données contient une unité de calcul (parfois plusieurs), des registres isolés, un banc de registre, une unité mémoire. Le tout est chapeauté par une unité de contrôle qui commande le chemin de données, qui fera l'objet des prochains chapitres. Mais il faut maintenant relier registres, ALU et unité mémoire pour que l'ensemble fonctionne. Pour cela, diverses interconnexions internes au processeur se chargent de relier le tout.
Sur les anciens processeurs, les interconnexions sont assez simples et se résument à un ou deux '''bus internes au processeur''', reliés au bus mémoire. C'était la norme sur des architectures assez ancienne, qu'on n'a pas encore vu à ce point du cours, appelées les architectures à accumulateur et à pile. Mais ce n'est plus la solution utilisée actuellement. De nos jours, le réseaux d'interconnexion intra-processeur est un ensemble de connexions point à point entre ALU/registres/unité mémoire. Et paradoxalement, cela rend plus facile de comprendre ce réseau d'interconnexion.
===Introduction propédeutique : l'implémentation des modes d'adressage principaux===
L'organisation interne du processeur dépend fortement des modes d'adressage supportés. Pour simplifier les explications, nous allons séparer les modes d'adressage qui gèrent les pointeurs et les autres. Suivant que le processeur supporte les pointeurs ou non, l'organisation des bus interne est légèrement différente. La différence se voit sur les connexions avec le bus d'adresse et de données.
Tout processeur gère au minimum le '''mode d'adressage absolu''', où l'adresse est intégrée à l'instruction. Le séquenceur extrait l'adresse mémoire de l'instruction, et l'envoie sur le bus d'adresse. Pour cela, le séquenceur est relié au bus d'adresse, le chemin de donnée est relié au bus de données. Le chemin de donnée n'est pas connecté au bus d'adresse, il n'y a pas d'autres connexions.
[[File:Chemin de données sans support des pointeurs.png|centre|vignette|upright=2|Chemin de données sans support des pointeurs]]
Le '''support des pointeurs''' demande d'intégrer des modes d'adressage dédiés : l'adressage indirect à registre, l'adresse base + indice, et les autres. Les pointeurs sont stockés dans le banc de registre et sont modifiés par l'unité de calcul. Pour supporter les pointeurs, le chemin de données est connecté sur le bus d'adresse avec le séquenceur. Suivant le mode d'adressage, le bus d'adresse est relié soit au chemin de données, soit au séquenceur.
[[File:Chemin de données avec support des pointeurs.png|centre|vignette|upright=2|Chemin de données avec support des pointeurs]]
Pour terminer, il faut parler des instructions de '''copie mémoire vers mémoire''', qui copient une donnée d'une adresse mémoire vers une autre. Elles ne se passent pas vraiment dans le chemin de données, mais se passent purement au niveau des registres d’interfaçage. L'usage d'un registre d’interfaçage unique permet d'implémenter ces instructions très facilement. Elle se fait en deux étapes : on copie la donnée dans le registre d’interfaçage, on l'écrit en mémoire RAM. L'adresse envoyée sur le bus d'adresse n'est pas la même lors des deux étapes.
===Le banc de registre est multi-port, pour gérer nativement les opérations dyadiques===
Les architectures RISC et CISC incorporent un banc de registre, qui est connecté aux unités de calcul et au bus mémoire. Et ce banc de registre peut être mono-port ou multiport. S'il a existé d'anciennes architectures utilisant un banc de registre mono-port, elles sont actuellement obsolètes. Nous les aborderons dans un chapitre dédié aux architectures dites canoniques, mais nous pouvons les laisser de côté pour le moment. De nos jours, tous les processeurs utilisent un banc de registre multi-port.
[[File:Chemin de données minimal d'une architecture LOAD-STORE (sans MOV inter-registres).png|centre|vignette|upright=2|Chemin de données minimal d'une architecture LOAD-STORE (sans MOV inter-registres)]]
Le banc de registre multiport est optimisé pour les opérations dyadiques. Il dispose précisément de deux ports de lecture et d'un port d'écriture pour l'écriture. Un port de lecture par opérande et le port d'écriture pour enregistrer le résultat. En clair, le processeur peut lire deux opérandes et écrire un résultat en un seul cycle d'horloge. L'avantage est que les opérations simples ne nécessitent qu'une micro-opération, pas plus.
[[File:ALU data paths.svg|centre|vignette|upright=1.5|Processeur LOAD-STORE avec un banc de registre multiport, avec les trois ports mis en évidence.]]
===Une architecture LOAD-STORE basique, avec adressage absolu===
Voyons maintenant comment l'implémentation d'une architecture RISC très simple, qui ne supporte pas les adressages pour les pointeurs, juste les adressages inhérent (à registres) et absolu (par adresse mémoire). Les instructions LOAD et STORE utilisent l'adressage absolu, géré par le séquenceur, reste à gérer l'échange entre banc de registres et bus de données. Une lecture LOAD relie le bus de données au port d'écriture du banc de registres, alors que l'écriture relie le bus au port de lecture du banc de registre. Pour cela, il faut ajouter des multiplexeurs sur les chemins existants, comme illustré par le schéma ci-dessous.
[[File:Bus interne au processeur sur archi LOAD STORE avec banc de registres multiport.png|centre|vignette|upright=2|Organisation interne d'une architecture LOAD STORE avec banc de registres multiport. Nous n'avons pas représenté les signaux de commandes envoyés par le séquenceur au chemin de données.]]
Ajoutons ensuite les instructions de copie entre registres, souvent appelées instruction COPY ou MOV. Elles existent sur la plupart des architectures LOAD-STORE. Une première solution boucle l'entrée du banc de registres sur son entrée, ce qui ne sert que pour les copies de registres.
[[File:Chemin de données d'une architecture LOAD-STORE.png|centre|vignette|upright=2|Chemin de données d'une architecture LOAD-STORE]]
Mais il existe une seconde solution, qui ne demande pas de modifier le chemin de données. Il est possible de faire passer les copies de données entre registres par l'ALU. Lors de ces copies, l'ALU une opération ''Pass through'', à savoir qu'elle recopie une des opérandes sur sa sortie. Le fait qu'une ALU puisse effectuer une opération ''Pass through'' permet de fortement simplifier le chemin de donnée, dans le sens où cela permet d'économiser des multiplexeurs. Mais nous verrons cela sous peu. D'ailleurs, dans la suite du chapitre, nous allons partir du principe que les copies entre registres passent par l'ALU, afin de simplifier les schémas.
===L'ajout des modes d'adressage indirects à registre pour les pointeurs===
Passons maintenant à l'implémentation des modes d'adressages pour les pointeurs. Avec eux, l'adresse mémoire à lire/écrire n'est pas intégrée dans une instruction, mais est soit dans un registre, soit calculée par l'ALU.
Le premier mode d'adressage de ce type est le mode d'adressage indirect à registre, où l'adresse à lire/écrire est dans un registre. L'implémenter demande donc de connecter la sortie du banc de registres au bus d'adresse. Il suffit d'ajouter un MUX en sortie d'un port de lecture.
[[File:Chemin de données à trois bus.png|centre|vignette|upright=2|Chemin de données à trois bus.]]
Le mode d'adressage base + indice est un mode d'adressage où l'adresse à lire/écrire est calculée à partir d'une adresse et d'un indice, tous deux présents dans un registre. Le calcul de l'adresse implique au minimum une addition et donc l'ALU. Dans ce cas, on doit connecter la sortie de l'unité de calcul au bus d'adresse.
[[File:Bus avec adressage base+index.png|centre|vignette|upright=2|Bus avec adressage base+index]]
Le chemin de données précédent gère aussi le mode d'adressage indirect avec pré-décrément. Pour rappel, ce mode d'adressage est une variante du mode d'adressage indirect, qui utilise une pointeur/adresse stocké dans un registre. La différence est que ce pointeur est décrémenté avant d'être envoyé sur le bus d'adresse. L'implémentation matérielle est la même que pour le mode Base + Indice : l'adresse est lue depuis les registres, décrémentée dans l'ALU, et envoyée sur le bus d'adresse.
Le schéma précédent montre que le bus d'adresse est connecté à un MUX avant l'ALU et un autre MUX après. Mais il est possible de se passer du premier MUX, utilisé pour le mode d'adressage indirect à registre. La condition est que l'ALU supporte l'opération ''pass through'', un NOP, qui recopie une opérande sur sa sortie. L'ALU fera une opération NOP pour le mode d'adressage indirect à registre, un calcul d'adresse pour le mode d'adressage base + indice. Par contre, faire ainsi rendra l'adressage indirect légèrement plus lent, vu que le temps de passage dans l'ALU sera compté.
[[File:Bus avec adressage indirect.png|centre|vignette|upright=2|Bus avec adressages pour les pointeurs, simplifié.]]
Dans ce qui va suivre, nous allons partir du principe que le processeur est implémenté en suivant le schéma précédent, afin d'avoir des schéma plus lisibles.
===L'adressage immédiat et les modes d'adressages exotiques===
Passons maintenant au mode d’adressage immédiat, qui permet de préciser une constante dans une instruction directement. La constante est extraite de l'instruction par le séquenceur, puis insérée au bon endroit dans le chemin de données. Pour les opérations arithmétiques/logiques/branchements, il faut insérer la constante extraite sur l'entrée de l'ALU. Sur certains processeurs, la constante peut être négative et doit alors subir une extension de signe dans un circuit spécialisé.
[[File:Chemin de données - Adressage immédiat avec extension de signe.png|centre|vignette|upright=2|Chemin de données - Adressage immédiat avec extension de signe.]]
L'implémentation précédente gère aussi les modes d'adressage base + décalage et absolu indexé. Pour rappel, le premier ajoute une constante à une adresse prise dans les registres, le second prend une adresse constante et lui ajoute un indice pris dans les registres. Dans les deux cas, on lit un registre, extrait une constante/adresse de l’instruction, additionne les deux dans l'ALU, avant d'envoyer le résultat sur le bus d'adresse. La seule difficulté est de désactiver l'extension de signe pour les adresses.
Le mode d'adressage absolu peut être traité de la même manière, si l'ALU est capable de faire des NOPs. L'adresse est insérée au même endroit que pour le mode d'adressage immédiat, parcours l'unité de calcul inchangée parce que NOP, et termine sur le bus d'adresse.
[[File:Chemin de données avec une ALU capable de faire des NOP.png|centre|vignette|upright=2|Chemin de données avec adressage immédiat étendu pour gérer des adresses.]]
Passons maintenant au cas particulier d'une instruction MOV qui copie une constante dans un registre. Il n'y a rien à faire si l'unité de calcul est capable d'effectuer une opération NOP/''pass through''. Pour charger une constante dans un registre, l'ALU est configurée pour faire un NOP, la constante traverse l'ALU et se retrouve dans les registres. Si l'ALU ne gère pas les NOP, la constante doit être envoyée sur l'entrée d'écriture du banc de registres, à travers un MUX dédié.
[[File:Implémentation de l'adressage immédiat dans le chemin de données.png|centre|vignette|upright=2|Implémentation de l'adressage immédiat dans le chemin de données]]
===Les architectures CISC : les opérations ''load-op''===
Tout ce qu'on a vu précédemment porte sur les processeurs de type LOAD-STORE, souvent confondus avec les processeurs de type RISC, où les accès mémoire sont séparés des instructions utilisant l'ALU. Il est maintenant temps de voir les processeurs CISC, qui gèrent des instructions ''load-op'', qui peuvent lire une opérande depuis la mémoire.
L'implémentation des opérations ''load-op'' relie le bus de donnée directement sur une entrée de l'unité de calcul, en utilisant encore une fois un multiplexeur. L'implémentation parait simple, mais c'est parce que toute la complexité est déportée dans le séquenceur. C'est lui qui se charge de détecter quand la lecture de l'opérande est terminée, quand l'opérande est disponible.
Les instructions ''load-op'' s'exécutent en plusieurs étapes, en plusieurs micro-opérations. Il y a typiquement une étape pour l'opérande à lire en mémoire et une étape de calcul. L'usage d'un registre d’interfaçage permet d'implémenter les instructions ''load-op'' très facilement. Une opération ''load-op'' charge l'opérande en mémoire dans un registre d’interfaçage, puis relier ce registre d’interfaçage sur une des entrées de l'ALU. Un simple multiplexeur suffit pour implémenter le tout, en plus des modifications adéquates du séquenceur.
[[File:Chemin de données d'un CPU CISC avec lecture des opérandes en mémoire.png|centre|vignette|upright=2|Chemin de données d'un CPU CISC avec lecture des opérandes en mémoire]]
Supporter les instructions multi-accès (qui font plusieurs accès mémoire) ne modifie pas fondamentalement le réseau d'interconnexion, ni le chemin de données La raison est que supporter les instructions multi-accès se fait au niveau du séquenceur. En réalité, les accès mémoire se font en série, l'un après l'autre, sous la commande du séquenceur qui émet plusieurs micro-opérations mémoire consécutives. Les données lues sont placées dans des registres d’interactivement mémoire, ce qui demande d'ajouter des registres d’interfaçage mémoire en plus.
==Annexe : le cas particulier du pointeur de pile==
Le pointeur de pile est un registre un peu particulier. Il peut être placé dans le chemin de données ou dans le séquenceur, voire dans l'unité de chargement, tout dépend du processeur. Tout dépend de si le pointeur de pile gère une pile d'adresses de retour ou une pile d'appel.
===Le pointeur de pile non-adressable explicitement===
Avec une pile d'adresse de retour, le pointeur de pile n'est pas adressable explicitement, il est juste adressé implicitement par des instructions d'appel de fonction CALL et des instructions de retour de fonction RET. Le pointeur de pile est alors juste incrémenté ou décrémenté par un pas constant, il ne subit pas d'autres opérations, son adressage est implicite. Il est juste incrémenté/décrémenté par pas constants, qui sont fournis par le séquenceur. Il n'y a pas besoin de le relier au chemin de données, vu qu'il n'échange pas de données avec les autres registres. Il y a alors plusieurs solutions, mais la plus simple est de placer le pointeur de pile dans le séquenceur et de l'incrémenter par un incrémenteur dédié.
Quelques processeurs simples disposent d'une pile d'appel très limitée, où le pointeur de pile n'est pas adressable explicitement. Il est adressé implicitement par les instruction CALL, RET, mais aussi PUSH et POP, mais aucune autre instruction ne permet cela. Là encore, le pointeur de pile ne communique pas avec les autres registres. Il est juste incrémenté/décrémenté par pas constants, qui sont fournis par le séquenceur. Là encore, le plus simple est de placer le pointeur de pile dans le séquenceur et de l'incrémenter par un incrémenteur dédié.
Dans les deux cas, le pointeur de pile est placé dans l'unité de contrôle, le séquenceur, et est associé à un incrémenteur dédié. Il se trouve que cet incrémenteur est souvent partagé avec le ''program counter''. En effet, les deux sont des adresses mémoire, qui sont incrémentées et décrémentées par pas constants, ne subissent pas d'autres opérations (si ce n'est des branchements, mais passons). Les ressemblances sont suffisantes pour fusionner les deux circuits. Ils peuvent donc avoir un '''incrémenteur partagé'''.
L'incrémenteur en question est donc partagé entre pointeur de pile, ''program counter'' et quelques autres registres similaires. Par exemple, le Z80 intégrait un registre pour le rafraichissement mémoire, qui était réalisé par le CPU à l'époque. Ce registre contenait la prochaine adresse mémoire à rafraichir, et était incrémenté à chaque rafraichissement d'une adresse. Et il était lui aussi intégré au séquenceur et incrémenté par l'incrémenteur partagé.
[[File:Organisation interne d'une architecture à pile.png|centre|vignette|upright=2|Organisation interne d'une architecture à pile]]
===Le pointeur de pile adressable explicitement===
Maintenant, étudions le cas d'une pile d'appel, précisément d'une pile d'appel avec des cadres de pile de taille variable. Sous ces conditions, le pointeur de pile est un registre adressable, avec un nom/numéro de registre dédié. Tel est par exemple le cas des processeurs x86 avec le registre ESP (''Extended Stack Pointer''). 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 calculs d'adresse, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres.
Dans ce cas, la meilleure solution est de placer le pointeur de pile dans le banc de registre généraux, avec les autres registres entiers. En faisant cela, la manipulation du pointeur de pile est faite par l'unité de calcul entière, pas besoin d'utiliser un incrémenteur dédiée. Il a existé des processeurs qui mettaient le pointeur de pile dans le banc de registre, mais l'incrémentaient avec un incrémenteur dédié, mais nous les verrons dans le chapitre sur les architectures à accumulateur. La raison est que sur les processeurs concernés, les adresses ne faisaient pas la même taille que les données : c'était des processeurs 8 bits, qui géraient des adresses de 16 bits.
==Annexe : l'implémentation du système d'''aliasing'' des registres des CPU x86==
Il y a quelques chapitres, nous avions parlé du système d'''aliasing'' des registres des CPU x86. Pour rappel, il permet de donner plusieurs noms de registre pour un même registre. Plus précisément, pour un registre 64 bits, le registre complet aura un nom de registre, les 32 bits de poids faible auront leur nom de registre dédié, idem pour les 16 bits de poids faible, etc. Il est possible de faire des calculs sur ces moitiés/quarts/huitièmes de registres sans problème.
===L'''aliasing'' du 8086, pour les registres 16 bits===
[[File:Register 8086.PNG|vignette|Register 8086]]
L'implémentation de l'''aliasing'' est apparue sur les premiers CPU Intel 16 bits, notamment le 8086. En tout, ils avaient quatre registres généraux 16 bits : AX, BX, CX et DX. Ces quatre registres 16 bits étaient coupés en deux octets, chacun adressable. Par exemple, le registre AX était coupé en deux octets nommés AH et AL, chacun ayant son propre nom/numéro de registre. Les instructions d'addition/soustraction pouvaient manipuler le registre AL, ou le registre AH, ce qui modifiait les 8 bits de poids faible ou fort selon le registre choisit.
Le banc de registre ne gére que 4 registres de 16 bits, à savoir AX, BX, CX et DX. Lors d'une lecture d'un registre 8 bits, le registre 16 bit entier est lu depuis le banc de registre, mais les bits inutiles sont ignorés. Par contre, l'écriture peut se faire soit avec 16 bits d'un coup, soit pour seulement un octet. Le port d'écriture du banc de registre peut être configuré de manière à autoriser l'écriture soit sur les 16 bits du registre, soit seulement sur les 8 bits de poids faible, soit écrire dans les 8 bits de poids fort.
[[File:Port d'écriture du banc de registre du 8086.png|centre|vignette|upright=2.5|Port d'écriture du banc de registre du 8086]]
Une opération sur un registre 8 bits se passe comme suit. Premièrement, on lit le registre 16 bits complet depuis le banc de registre. Si l'on a sélectionné l'octet de poids faible, il ne se passe rien de particulier, l'opérande 16 bits est envoyée directement à l'ALU. Mais si on a sélectionné l'octet de poids fort, la valeur lue est décalée de 7 rangs pour atterrir dans les 8 octets de poids faible. Ensuite, l'unité de calcul fait un calcul avec cet opérande, un calcul 16 bits tout ce qu'il y a de plus classique. Troisièmement, le résultat est enregistré dans le banc de registre, en le configurant convenablement. La configuration précise s'il faut enregistrer le résultat dans un registre 16 bits, soit seulement dans l'octet de poids faible/fort.
Afin de simplifier le câblage, les 16 bits des registres AX/BX/CX/DX sont entrelacés d'une manière un peu particulière. Intuitivement, on s'attend à ce que les bits soient physiquement dans le même ordre que dans le registre : le bit 0 est placé à côté du bit 1, suivi par le bit 2, etc. Mais à la place, l'octet de poids fort et de poids faible sont mélangés. Deux bits consécutifs appartiennent à deux octets différents. Le tout est décrit dans le tableau ci-dessous.
{|class="wikitable"
|-
! Registre 16 bits normal
| class="f_bleu" | 15
| class="f_bleu" | 14
| class="f_bleu" | 13
| class="f_bleu" | 12
| class="f_bleu" | 11
| class="f_bleu" | 10
| class="f_bleu" | 9
| class="f_bleu" | 8
| class="f_rouge" | 7
| class="f_rouge" | 6
| class="f_rouge" | 5
| class="f_rouge" | 4
| class="f_rouge" | 3
| class="f_rouge" | 2
| class="f_rouge" | 1
| class="f_rouge" | 0
|-
! Registre 16 bits du 8086
| class="f_bleu" | 15
| class="f_rouge" | 7
| class="f_bleu" | 14
| class="f_rouge" | 6
| class="f_bleu" | 13
| class="f_rouge" | 5
| class="f_bleu" | 12
| class="f_rouge" | 4
| class="f_bleu" | 11
| class="f_rouge" | 3
| class="f_bleu" | 10
| class="f_rouge" | 2
| class="f_bleu" | 9
| class="f_rouge" | 1
| class="f_bleu" | 8
| class="f_rouge" | 0
|}
En faisant cela, le décaleur en entrée de l'ALU est bien plus simple. Il y a 8 multiplexeurs, mais le câblage est bien plus simple. Par contre, en sortie de l'ALU, il faut remettre les bits du résultat dans l'ordre adéquat, celui du registre 8086. Pour cela, les interconnexions sur le port d'écriture sont conçues pour. Il faut juste mettre les fils de sortie de l'ALU sur la bonne entrée, par besoin de multiplexeurs.
===L'''aliasing'' sur les processeurs x86 32/64 bits===
Les processeurs x86 32 et 64 bits ont un système d'''aliasing'' qui complète le système précédent. Les processeurs 32 bits étendent les registres 16 bits existants à 32 bits. Pour ce faire, le registre 32 bit a un nouveau nom de registre, distincts du nom de registre utilisé pour l'ancien registre 16 bits. Il est possible d'adresser les 16 bits de poids faible de ce registre, avec le même nom de registre que celui utilisé pour le registre 16 sur les processeurs d'avant. Même chose avec les processeurs 64, avec l'ajout d'un nouveau nom de registre pour adresser un registre de 64 bit complet.
En soit, implémenter ce système n'est pas compliqué. Prenons le cas du registre RAX (64 bits), et de ses subdivisions nommées EAX (32 bits), AX (16 bits). À l'intérieur du banc de registre, il n'y a que le registre RAX. Le banc de registre ne comprend qu'un seul nom de registre : RAX. Les subdivisions EAX et AX n'existent qu'au niveau de l'écriture dans le banc de registre. L'écriture dans le banc de registre est configurable, de manière à ne modifier que les bits adéquats. Le résultat d'un calcul de l'ALU fait 64 bits, il est envoyé sur le port d'écriture. À ce niveau, soit les 64 bits sont écrits dans le registre, soit seulement les 32/16 bits de poids faible. Le système du 8086 est préservé pour les écritures dans les 16 bits de poids faible.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les composants d'un processeur
| prevText=Les composants d'un processeur
| next=L'unité de chargement et le program counter
| nextText=L'unité de chargement et le program counter
}}
</noinclude>
dn82tejrkcjiub7dzir71hyz5ne02vs
745834
745833
2025-07-02T20:40:29Z
Mewtow
31375
/* Les unités de calcul spécialisées */
745834
wikitext
text/x-wiki
Comme vu précédemment, le '''chemin de donnée''' est l'ensemble des composants dans lesquels circulent les données dans le processeur. Il comprend l'unité de calcul, les registres, l'unité de communication avec la mémoire, et le ou les interconnexions qui permettent à tout ce petit monde de communiquer. Dans ce chapitre, nous allons voir ces composants en détail.
==Les unités de calcul==
Le processeur contient des circuits capables de faire des calculs arithmétiques, des opérations logiques, et des comparaisons, qui sont regroupés dans une unité de calcul appelée '''unité arithmétique et logique'''. Certains préfèrent l’appellation anglaise ''arithmetic and logic unit'', ou ALU. Par défaut, ce terme est réservé aux unités de calcul qui manipulent des nombres entiers. Les unités de calcul spécialisées pour les calculs flottants sont désignées par le terme "unité de calcul flottant", ou encore FPU (''Floating Point Unit'').
L'interface d'une unité de calcul est assez simple : on a des entrées pour les opérandes et une sortie pour le résultat du calcul. De plus, les instructions de comparaisons ou de calcul peuvent mettre à jour le registre d'état, qui est relié à une autre sortie de l’unité de calcul. Une autre entrée, l''''entrée de sélection de l'instruction''', spécifie l'opération à effectuer. Elle sert à configurer l'unité de calcul pour faire une addition et pas une multiplication, par exemple. Sur cette entrée, on envoie un numéro qui précise l'opération à effectuer. La correspondance entre ce numéro et l'opération à exécuter dépend de l'unité de calcul. Sur les processeurs où l'encodage des instructions est "simple", une partie de l'opcode de l'instruction est envoyé sur cette entrée.
[[File:Unité de calcul usuelle.png|centre|vignette|upright=2|Unité de calcul usuelle.]]
Il faut signaler que les processeurs modernes possèdent plusieurs unités de calcul, toutes reliées aux registres. Cela permet d’exécuter plusieurs calculs en même temps dans des unités de calcul différentes, afin d'augmenter les performances du processeur. Diverses technologies, abordées dans la suite du cours permettent de profiter au mieux de ces unités de calcul : pipeline, exécution dans le désordre, exécution superscalaire, jeux d'instructions VLIW, etc. Mais laissons cela de côté pour le moment.
===L'ALU entière : additions, soustractions, opérations bit à bit===
Un processeur contient plusieurs ALUs spécialisées. La principale, présente sur tous les processeurs, est l''''ALU entière'''. Elle s'occupe uniquement des opérations sur des nombres entiers, les nombres flottants sont gérés par une ALU à part. Elle gère des opérations simples : additions, soustractions, opérations bit à bit, parfois des décalages/rotations. Par contre, elle ne gère pas la multiplication et la division, qui sont prises en charge par un circuit multiplieur/diviseur à part.
L'ALU entière a déjà été vue dans un chapitre antérieur, nommé "Les unités arithmétiques et logiques entières (simples)", qui expliquait comment en concevoir une. Nous avions vu qu'une ALU entière est une sorte de circuit additionneur-soustracteur amélioré, ce qui explique qu'elle gère des opérations entières simples, mais pas la multiplication ni la division. Nous ne reviendrons pas dessus. Cependant, il y a des choses à dire sur leur intégration au processeur.
Une ALU entière gère souvent une opération particulière, qui ne fait rien et recopie simplement une de ses opérandes sur sa sortie. L'opération en question est appelée l''''opération ''Pass through''''', encore appelée opération NOP. Elle est implémentée en utilisant un simple multiplexeur, placé en sortie de l'ALU. Le fait qu'une ALU puisse effectuer une opération ''Pass through'' permet de fortement simplifier le chemin de donnée, d'économiser des multiplexeurs. Mais nous verrons cela sous peu.
[[File:ALU avec opération NOP.png|centre|vignette|upright=2|ALU avec opération NOP.]]
Avant l'invention du microprocesseur, le processeur n'était pas un circuit intégré unique. L'ALU, le séquenceur et les registres étaient dans des puces séparées. Les ALU étaient vendues séparément et manipulaient des opérandes de 4/8 bits, les ALU 4 bits étaient très fréquentes. Si on voulait créer une ALU pour des opérandes plus grandes, il fallait construire l'ALU en combinant plusieurs ALU 4/8 bits. Par exemple, l'ALU des processeurs AMD Am2900 est une ALU de 16 bits composée de plusieurs sous-ALU de 4 bits. Cette technique qui consiste à créer des unités de calcul à partir d'unités de calcul plus élémentaires s'appelle en jargon technique du '''bit slicing'''. Nous en avions parlé dans le chapitre sur les unités de calcul, aussi nous n'en reparlerons pas plus ici.
L'ALU manipule des opérandes codées sur un certain nombre de bits. Par exemple, une ALU peut manipuler des entiers codés sur 8 bits, sur 16 bits, etc. En général, la taille des opérandes de l'ALU est la même que la taille des registres. Un processeur 32 bits, avec des registres de 32 bit, a une ALU de 32 bits. C'est intuitif, et cela rend l'implémentation du processeur bien plus facile. Mais il y a quelques exceptions, où l'ALU manipule des opérandes plus petits que la taille des registres. Par exemple, de nombreux processeurs 16 bits, avec des registres de 16 bits, utilisent une ALU de 8 bits. Un autre exemple assez connu est celui du Motorola 68000, qui était un processeur 32 bits, mais dont l'ALU faisait juste 16 bits. Son successeur, le 68020, avait lui une ALU de 32 bits.
Sur de tels processeurs, les calculs sont fait en plusieurs passes. Par exemple, avec une ALU 8 bit, les opérations sur des opérandes 8 bits se font en un cycle d'horloge, celles sur 16 bits se font en deux cycles, celles en 32 en quatre, etc. Si un programme manipule assez peu d'opérandes 16/32/64 bits, la perte de performance est assez faible. Diverses techniques visent à améliorer les performances, mais elles ne font pas de miracles. Par exemple, vu que l'ALU est plus courte, il est possible de la faire fonctionner à plus haute fréquence, pour réduire la perte de performance.
Pour comprendre comme est implémenté ce système de passes, prenons l'exemple du processeur 8 bit Z80. Ses registres entiers étaient des registres de 8 bits, alors que l'ALU était de 4 bits. Les calculs étaient faits en deux phases : une qui traite les 4 bits de poids faible, une autre qui traite les 4 bits de poids fort. Pour cela, les opérandes étaient placées dans des registres de 4 bits en entrée de l'ALU, plusieurs multiplexeurs sélectionnaient les 4 bits adéquats, le résultat était mémorisé dans un registre de résultat de 8 bits, un démultiplexeur plaçait les 4 bits du résultat au bon endroit dans ce registre. L'unité de contrôle s'occupait de la commande des multiplexeurs/démultiplexeurs. Les autres processeurs 8 ou 16 bits utilisent des circuits similaires pour faire leurs calculs en plusieurs fois.
[[File:ALU du Z80.png|centre|vignette|upright=2|ALU du Z80]]
Un exemple extrême est celui des des '''processeurs sériels''' (sous-entendu ''bit-sériels''), qui utilisent une '''ALU sérielle''', qui fait leurs calculs bit par bit, un bit à la fois. S'il a existé des processeurs de 1 bit, comme le Motorola MC14500B, la majeure partie des processeurs sériels étaient des processeurs 4, 8 ou 16 bits. L'avantage de ces ALU est qu'elles utilisent peu de transistors, au détriment des performances par rapport aux processeurs non-sériels. Mais un autre avantage est qu'elles peuvent gérer des opérandes de grande taille, avec plus d'une trentaine de bits, sans trop de problèmes.
===Les circuits multiplieurs et diviseurs===
Les processeurs modernes ont une ALU pour les opérations simples (additions, décalages, opérations logiques), couplée à une ALU pour les multiplications, un circuit multiplieur séparé. Précisons qu'il ne sert pas à grand chose de fusionner le circuit multiplieur avec l'ALU, mieux vaut les garder séparés par simplicité. Les processeurs haute performance disposent systématiquement d'un circuit multiplieur et gèrent la multiplication dans leur jeu d'instruction.
Le cas de la division est plus compliqué. La présence d'un circuit multiplieur est commune, mais les circuits diviseurs sont eux très rares. Leur cout en circuit est globalement le même que pour un circuit multiplieur, mais le gain en performance est plus faible. Le gain en performance pour la multiplication est modéré car il s'agit d'une opération très fréquente, alors qu'il est très faible pour la division car celle-ci est beaucoup moins fréquente.
Pour réduire le cout en circuits, il arrive que l'ALU pour les multiplications gère à la fois la multiplication et la division. Les circuits multiplieurs et diviseurs sont en effet très similaires et partagent beaucoup de points communs. Généralement, la fusion se fait pour les multiplieurs/diviseurs itératifs.
Il existe cependant des circuits qui se passent de multiplieurs, tout en supportant la multiplication dans leur jeu d'instruction. Certains utilisent pour cela du microcode, technique qu'on verra dans deux chapitres, mais l'Intel Atom utilise une technique franchement peu ordinaire. L'Intel Atom utilise l'unité de calcul flottante pour faire les multiplications entières. Les opérandes entières sont traduites en nombres flottants, multipliés par l'unité de calcul flottante, puis le résultat est converti en un entier avec quelques corrections à la clé. Ainsi, on fait des économies de circuits, en mutualisant le multiplieur entre l'unité de calcul flottante et l'ALU entière, surtout que ce multiplieur manipule des opérandes plus courtes. Les performances sont cependant réduites comparé à l'usage d'un vrai multiplieur entier.
===Le ''barrel shifter''===
On vient d'expliquer que la présence de plusieurs ALU spécialisée est très utile pour implémenter des opérations compliquées à insérer dans une unité de calcul normale, comme la multiplication et la division. Mais les décalages sont aussi dans ce cas, de même que les rotations. Nous avions vu il y a quelques chapitres qu'ils sont réalisés par un circuit spécialisé, appelé un ''barrel shifter'', qu'il est difficile de fusionner avec une ALU normale. Aussi, beaucoup de processeurs incorporent un ''barrel shifter'' séparé de l'ALU.
Les processeurs ARM utilise un ''barrel shifter'', mais d'une manière un peu spéciale. On a vu il y a quelques chapitres que si on fait une opération logique, une addition, une soustraction ou une comparaison, la seconde opérande peut être décalée automatiquement. L'instruction incorpore le type de de décalage à faire et par combien de rangs il faut décaler directement à côté de l'opcode. Cela simplifie grandement les calculs d'adresse, qui se font en une seule instruction, contre deux ou trois sur d'autres architectures. Et pour cela, l'ALU proprement dite est précédée par un ''barrel shifter'',une seconde ALU spécialisée dans les décalages. Notons que les instructions MOV font aussi partie des instructions où la seconde opérande (le registre source) peut être décalé : cela signifie que les MOV passent par l'ALU, qui effectue alors un NOP, une opération logique OUI.
===Les unités de calcul spécialisées===
Un processeur peut disposer d’unités de calcul séparées de l'unité de calcul principale, spécialisées dans les décalages, les divisions, etc. Et certaines d'entre elles sont spécialisées dans des opérations spécifiques, qui ne sont techniquement pas des opérations entières, sur des nombres entiers.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Depuis les années 90-2000, presque tous les processeurs utilisent une unité de calcul spécialisée pour les nombres flottants : la '''Floating-Point Unit''', aussi appelée FPU. En général, elle regroupe un additionneur-soustracteur flottant et un multiplieur flottant. Parfois, elle incorpore un diviseur flottant, tout dépend du processeur. Précisons que sur certains processeurs, la FPU et l'ALU entière ne vont pas à la même fréquence, pour des raisons de performance et de consommation d'énergie !
La FPU intègre un circuit multiplieur entier, utilisé pour les multiplications flottantes, afin de multiplier les mantisses entre elles. Quelques processeurs utilisaient ce multiplier pour faire les multiplications entières. En clair, au lieu d'avoir un multiplieur entier séparé du multiplieur flottant, les deux sont fusionnés en un seul circuit. Il s'agit d'une optimisation qui a été utilisée sur quelques processeurs 32 bits, qui supportaient les flottants 64 bits (double précision). Les processeurs Atom étaient dans ce cas, idem pour l'Athlon première génération. Les processeurs modernes n'utilisent pas cette optimisation pour des raisons qu'on ne peut pas expliquer ici (réduction des dépendances structurelles, émission multiple).
Il existe des unités de calcul spécialisées pour les calculs d'adresse. Elles ne supportent guère plus que des incrémentations/décrémentations, des additions/soustractions, et des décalages simples. Les autres opérations n'ont pas de sens avec des adresses. L'usage d'ALU spécialisées pour les adresses est un avantage sur les processeurs où les adresses ont une taille différente des données, ce qui est fréquent sur les anciens processeurs 8 bits.
De nombreux processeurs modernes disposent d'une unité de calcul spécialisée dans le calcul des conditions, tests et branchements. C’est notamment le cas sur les processeurs sans registre d'état, qui disposent de registres à prédicats. En général, les registres à prédicats sont placés à part des autres registres, dans un banc de registre séparé. L'unité de calcul normale n'est pas reliée aux registres à prédicats, alors que l'unité de calcul pour les branchements/test/conditions l'est. les registres à prédicats sont situés juste en sortie de cette unité de calcul.
==Les registres du processeur==
Après avoir vu l'unité de calcul, il est temps de passer aux registres d'un processeur. L'organisation des registres est généralement assez compliquée, avec quelques registres séparés des autres comme le registre d'état ou le ''program counter''. Les registres d'un processeur peuvent se classer en deux camps : soit ce sont des registres isolés, soit ils sont regroupés en paquets appelés banc de registres.
Un '''banc de registres''' (''register file'') est une RAM, dont chaque byte est un registre. Il regroupe un paquet de registres différents dans un seul composant, dans une seule mémoire. Dans processeur moderne, on trouve un ou plusieurs bancs de registres. La répartition des registres, à savoir quels registres sont dans le banc de registre et quels sont ceux isolés, est très variable suivant les processeurs.
[[File:Register File Simple.svg|centre|vignette|upright=1|Banc de registres simplifié.]]
===L'adressage du banc de registres===
Le banc de registre est une mémoire comme une autre, avec une entrée d'adresse qui permet de sélectionner le registre voulu. Plutot que d'adresse, nous allons parler d''''identifiant de registre'''. Le séquenceur forge l'identifiant de registre en fonction des registres sélectionnés. Dans les chapitres précédents, nous avions vu qu'il existe plusieurs méthodes pour sélectionner un registre, qui portent les noms de modes d'adressage. Et bien les modes d'adressage jouent un grand rôle dans la forge de l'identifiant de registre.
Pour rappel, sur la quasi-totalité des processeurs actuels, les registres généraux sont identifiés par un nom de registre, terme trompeur vu que ce nom est en réalité un numéro. En clair, les processeurs numérotent les registres, le numéro/nom du registre permettant de l'identifier. Par exemple, si je veux faire une addition, je dois préciser les deux registres pour les opérandes, et éventuellement le registre pour le résultat : et bien ces registres seront identifiés par un numéro. Mais tous les registres ne sont pas numérotés et ceux qui ne le sont pas sont adressés implicitement. Par exemple, le pointeur de pile sera modifié par les instructions qui manipulent la pile, sans que cela aie besoin d'être précisé par un nom de registre dans l'instruction.
Dans le cas le plus simple, les registres nommés vont dans le banc de registres, les registres adressés implicitement sont en-dehors, dans des registres isolés. L'idéntifiant de registre est alors simplement le nom de registre, le numéro. Le séquenceur extrait ce nom de registre de l'insutrction, avant de l'envoyer sur l'entrée d'adresse du banc de registre.
[[File:Adressage du banc de registres généruax.png|centre|vignette|upright=2|Adressage du banc de registres généraux]]
Dans un cas plus complexe, des registres non-nommés sont placés dans le banc de registres. Par exemple, les pointeurs de pile sont souvent placés dans le banc de registre, même s'ils sont adressés implicitement. Même des registres aussi importants que le ''program counter'' peuvent se mettre dans le banc de registre ! Nous verrons le cas du ''program counter'' dans le chapitre suivant, qui porte sur l'unité de chargement. Dans ce cas, le séquenceur forge l'identifiant de registre de lui-même. Dans le cas des registres nommés, il ajoute quelques bits aux noms de registres. Pour les registres adressés implicitement, il forge l'identifiant à partir de rien.
[[File:Adressage du banc de registre - cas général.png|centre|vignette|upright=2|Adressage du banc de registre - cas général]]
Nous verrons plus bas que dans certains cas, le nom de registre ne suffit pas à adresser un registre dans un banc de registre. Dans ce cas, le séquenceur rajoute des bits, comme dans l'exemple précédent. Tout ce qu'il faut retenir est que l'identifiant de registre est forgé par le séquenceur, qui se base entre autres sur le nom de registre s'il est présent, sur l'instruction exécutée dans le cas d'un registre adressé implicitement.
===Les registres généraux===
Pour rappel, les registres généraux peuvent mémoriser des entiers, des adresses, ou toute autre donnée codée en binaire. Ils sont souvent séparés des registres flottants sur les architectures modernes. Les registres généraux sont rassemblés dans un banc de registre dédié, appelé le '''banc de registres généraux'''. Le banc de registres généraux est une mémoire multiport, avec au moins un port d'écriture et deux ports de lecture. La raison est que les instructions lisent deux opérandes dans les registres et enregistrent leur résultat dans des registres. Le tout se marie bien avec un banc de registre à deux de lecture (pour les opérandes) et un d'écriture (pour le résultat).
[[File:Banc de registre multiports.png|centre|vignette|upright=2|Banc de registre multiports.]]
L'interface exacte dépend de si l'architecture est une architecture 2 ou 3 adresses. Pour rappel, la différence entre les deux tient dans la manière dont on précise le registre où enregistrer le résultat d'une opération. Avec les architectures 2-adresses, on précise deux registres : le premier sert à la fois comme opérande et pour mémoriser le résultat, l'autre sert uniquement d'opérande. Un des registres est donc écrasé pour enregistrer le résultat. Sur les architecture 3-adresses, on précise trois registres : deux pour les opérandes, un pour le résultat.
Les architectures 2-adresses ont un banc de registre où on doit préciser deux "adresses", deux noms de registre. L'interface du banc de registre est donc la suivante :
[[File:Register File Medium.svg|centre|vignette|upright=1.5|Register File d'une architecture à 2-adresses]]
Les architectures 3-adresses doivent rajouter une troisième entrée pour préciser un troisième nom de registre. L'interface du banc de registre est donc la suivante :
[[File:Register File Large.svg|centre|vignette|upright=1.5|Register File d'une architecture à 3-adresses]]
Rien n'empêche d'utiliser plusieurs bancs de registres sur un processeur qui utilise des registres généraux. La raison est une question d'optimisation. Au-delà d'un certain nombre de registres, il devient difficile d'utiliser un seul gros banc de registres. Il faut alors scinder le banc de registres en plusieurs bancs de registres séparés. Le problème est qu'il faut prévoir de quoi échanger des données entre les bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue du langage machine. Sur d'autres processeurs, les transferts de données entre bancs de registres se font via une instruction spéciale, souvent appelée COPY.
===Les registres flottants : banc de registre séparé ou unifié===
Passons maintenant aux registres flottants. Intuitivement, on a des registres séparés pour les entiers et les flottants. Il est alors plus simple d'utiliser un banc de registres séparé pour les nombres flottants, à côté d'un banc de registre entiers. L'avantage est que les nombres flottants et entiers n'ont pas forcément la même taille, ce qui se marie bien avec deux bancs de registres, où la taille des registres est différente dans les deux bancs.
Mais d'autres processeurs utilisent un seul '''banc de registres unifié''', qui regroupe tous les registres de données, qu'ils soient entier ou flottants. Par exemple, c'est le cas des Pentium Pro, Pentium II, Pentium III, ou des Pentium M : ces processeurs ont des registres séparés pour les flottants et les entiers, mais ils sont regroupés dans un seul banc de registres. Avec cette organisation, un registre flottant et un registre entier peuvent avoir le même nom de registre en langage machine, mais l'adresse envoyée au banc de registres ne doit pas être la même : le séquenceur ajoute des bits au nom de registre pour former l'adresse finale.
[[File:Désambiguïsation de registres sur un banc de registres unifié.png|centre|vignette|upright=2|Désambiguïsation de registres sur un banc de registres unifié.]]
===Le registre d'état===
Le registre d'état fait souvent bande à part et n'est pas placé dans un banc de registres. En effet, le registre d'état est très lié à l'unité de calcul. Il reçoit des indicateurs/''flags'' provenant de la sortie de l'unité de calcul, et met ceux-ci à disposition du reste du processeur. Son entrée est connectée à l'unité de calcul, sa sortie est reliée au séquenceur et/ou au bus interne au processeur.
Le registre d'état est relié au séquenceur afin que celui-ci puisse gérer les instructions de branchement, qui ont parfois besoin de connaitre certains bits du registre d'état pour savoir si une condition a été remplie ou non. D'autres processeurs relient aussi le registre d'état au bus interne, ce qui permet de lire son contenu et de le copier dans un registre de données. Cela permet d'implémenter certaines instructions, notamment celles qui permettent de mémoriser le registre d'état dans un registre général.
[[File:Place du registre d'état dans le chemin de données.png|centre|vignette|upright=2|Place du registre d'état dans le chemin de données]]
L'ALU fournit une sortie différente pour chaque bit du registre d'état, la connexion du registre d'état est directe, comme indiqué dans le schéma suivant. Vous remarquerez que le bit de retenue est à la fois connecté à la sortie de l'ALU, mais aussi sur son entrée. Ainsi, le bit de retenue calculé par une opération peut être utilisé pour la suivante. Sans cela, diverses instructions comme les opérations ''add with carry'' ne seraient pas possibles.
[[File:AluStatusRegister.svg|centre|vignette|upright=2|Registre d'état et unit de calcul.]]
Il est techniquement possible de mettre le registre d'état dans le banc de registre, pour économiser un registre. La principale difficulté est que les instructions doivent faire deux écritures dans le banc de registre : une pour le registre de destination, une pour le registre d'état. Soit on utilise deux ports d'écriture, soit on fait les deux écritures l'une après l'autre. Dans les deux cas, le cout en performances et en transistors n'en vaut pas le cout. D'ailleurs, je ne connais aucun processeur qui utilise cette technique.
Il faut noter que le registre d'état n'existe pas forcément en tant que tel dans le processeur. Quelques processeurs, dont le 8086 d'Intel, utilisent des bascules dispersées dans le processeur au lieu d'un vrai registre d'état. Les bascules dispersées mémorisent chacune un bit du registre d'état et sont placées là où elles sont le plus utile. Les bascules utilisées pour les branchements sont proches du séquenceur, le bascules pour les bits de retenue sont placées proche de l'ALU, etc.
===Les registres à prédicats===
Les registres à prédicats remplacent le registre d'état sur certains processeurs. Pour rappel, les registres à prédicat sont des registres de 1 bit qui mémorisent les résultats des comparaisons et instructions de test. Ils sont nommés/numérotés, mais les numéros en question sont distincts de ceux utilisés pour les registres généraux.
Ils sont placés à part, dans un banc de registres séparé. Le banc de registres à prédicats a une entrée de 1 bit connectée à l'ALU et une sortie de un bit connectée au séquenceur. Le banc de registres à prédicats est parfois relié à une unité de calcul spécialisée dans les conditions/instructions de test. Pour rappel, certaines instructions permettent de faire un ET, un OU, un XOR entre deux registres à prédicats. Pour cela, l'unité de calcul dédiée aux conditions peut lire les registres à prédicats, pour combiner le contenu de plusieurs d'entre eux.
[[File:Banc de registre pour les registres à prédicats.png|centre|vignette|upright=2|Banc de registre pour les registres à prédicats]]
===Les registres dédiés aux interruptions===
Dans le chapitre sur les registres, nous avions vu que certains processeurs dupliquaient leurs registres architecturaux, pour accélérer les interruptions ou les appels de fonction. Dans le cas qui va nous intéresser, les interruptions avaient accès à leurs propres registres, séparés des registres architecturaux. Les processeurs de ce type ont deux ensembles de registres identiques : un dédié aux interruptions, un autre pour les programmes normaux. Les registres dans les deux ensembles ont les mêmes noms, mais le processeur choisit le bon ensemble suivant s'il est dans une interruption ou non. Si on peut utiliser deux bancs de registres séparés, il est aussi possible d'utiliser un banc de registre unifié pour les deux.
Sur certains processeurs, le banc de registre est dupliqué en plusieurs exemplaires. La technique est utilisée pour les interruptions. Certains processeurs ont deux ensembles de registres identiques : un dédié aux interruptions, un autre pour les programmes normaux. Les registres dans les deux ensembles ont les mêmes noms, mais le processeur choisit le bon ensemble suivant s'il est dans une interruption ou non. On peut utiliser deux bancs de registres séparés, un pour les interruptions, et un pour les programmes.
Sur d'autres processeurs, on utilise un banc de registre unifié pour les deux ensembles de registres. Les registres pour les interruptions sont dans les adresses hautes, les registres pour les programmes dans les adresses basses. Le choix entre les deux est réalisé par un bit qui indique si on est dans une interruption ou non, disponible dans une bascule du processeur. Appelons là la bascule I.
===Le fenêtrage de registres===
[[File:Fenetre de registres.png|vignette|upright=1|Fenêtre de registres.]]
Le '''fenêtrage de registres''' fait que chaque fonction a accès à son propre ensemble de registres, sa propre fenêtre de registres. Là encore, cette technique duplique chaque registre architectural en plusieurs exemplaires qui portent le même nom. Chaque ensemble de registres architecturaux forme une fenêtre de registre, qui contient autant de registres qu'il y a de registres architecturaux. Lorsqu'une fonction s’exécute, elle se réserve une fenêtre inutilisée, et peut utiliser les registres de la fenêtre comme bon lui semble : une fonction manipule le registre architectural de la fenêtre réservée, mais pas les registres avec le même nom dans les autres fenêtres.
Il peut s'implémenter soit avec un banc de registres unifié, soit avec un banc de registre par fenêtre de registres.
Il est possible d'utiliser des bancs de registres dupliqués pour le fenêtrage de registres. Chaque fenêtre de registre a son propre banc de registres. Le choix entre le banc de registre à utiliser est fait par un registre qui mémorise le numéro de la fenêtre en cours. Ce registre commande un multiplexeur qui permet de choisir le banc de registre adéquat.
[[File:Fenêtrage de registres au niveau du banc de registres.png|vignette|Fenêtrage de registres au niveau du banc de registres.]]
L'utilisation d'un banc de registres unifié permet d'implémenter facilement le fenêtrage de registres. Il suffit pour cela de regrouper tous les registres des différentes fenêtres dans un seul banc de registres. Il suffit de faire comme vu au-dessus : rajouter des bits au nom de registre pour faire la différence entre les fenêtres. Cela implique de se souvenir dans quelle fenêtre de registre on est actuellement, cette information étant mémorisée dans un registre qui stocke le numéro de la fenêtre courante. Pour changer de fenêtre, il suffit de modifier le contenu de ce registre lors d'un appel ou retour de fonction avec un petit circuit combinatoire. Bien sûr, il faut aussi prendre en compte le cas où ce registre déborde, ce qui demande d'ajouter des circuits pour gérer la situation.
[[File:Désambiguïsation des fenêtres de registres.png|centre|vignette|upright=2|Désambiguïsation des fenêtres de registres.]]
==L'interface de communication avec la mémoire==
L''''interface avec la mémoire''' est, comme son nom l'indique, des circuits qui servent d'intermédiaire entre le bus mémoire et le processeur. Elle est parfois appelée l'unité mémoire, l'unité d'accès mémoire, la ''load-store unit'', et j'en oublie.
[[File:Unité de communication avec la mémoire, de type simple port.png|centre|vignette|upright=2|Unité de communication avec la mémoire, de type simple port.]]
Sur certains processeurs, elle gère les mémoires multiport.
[[File:Unité de communication avec la mémoire, de type multiport.png|centre|vignette|upright=2|Unité de communication avec la mémoire, de type multiport.]]
===Les registres d'interfaçage mémoire===
L'interface mémoire se résume le plus souvent à des '''registres d’interfaçage mémoire''', intercalés entre le bus mémoire et le chemin de données. Généralement, il y a au moins deux registres d’interfaçage mémoire : un registre relié au bus d'adresse, et autre relié au bus de données.
[[File:Registres d’interfaçage mémoire.png|centre|vignette|upright=2|Registres d’interfaçage mémoire.]]
Au lieu de lire ou écrire directement sur le bus, le processeur lit ou écrit dans ces registres, alors que l'unité d'accès mémoire s'occupe des échanges entre registres et bus mémoire. Lors d'une écriture, le processeur place l'adresse dans le registre d'interfaçage d'adresse, met la donnée à écrire dans le registre d'interfaçage de donnée, puis laisse l'unité d'accès mémoire faire son travail. Lors d'une lecture, il place l'adresse à lire sur le registre d'interfaçage d'adresse, il attend que la donnée soit lue, puis récupère la donnée dans le registre d'interfaçage de données.
L'avantage est que le processeur n'a pas à maintenir une donnée/adresse sur le bus durant tout un accès mémoire. Par exemple, prenons le cas où la mémoire met 15 cycles processeurs pour faire une lecture ou une écriture. Sans registres d'interfaçage mémoire, le processeur doit maintenir l'adresse durant 15 cycles, et aussi la donnée dans le cas d'une écriture. Avec ces registres, le processeur écrit dans les registres d'interfaçage mémoire au premier cycle, et passe les 14 cycles suivants à faire quelque chose d'autre. Par exemple, il faut faire un calcul en parallèle, envoyer des signaux de commande au banc de registre pour qu'il soit prêt une fois la donnée lue arrivée, etc. Cet avantage simplifie l'implémentation de certains modes d'adressage, comme on le verra à la fin du chapitre.
===L'unité de calcul d'adresse===
Les registres d'interfaçage sont presque toujours présents, mais le circuit que nous allons voir est complétement facultatif. Il s'agit d'une unité de calcul spécialisée dans les calculs d'adresse, dont nous avons parlé rapidement dans la section sur les ALU. Elle s'appelle l''''''Address generation unit''''', ou AGU. Elle est parfois séparée de l'interface mémoire proprement dit, et est alors considérée comme une unité de calcul à part, mais elle est généralement intimement liée à l'interface mémoire.
Elle sert pour certains modes d'adressage, qui demandent de combiner une adresse avec soit un indice, soit un décalage, plus rarement les deux. Les calculs d'adresse demandent de simplement incrémenter/décrémenter une adresse, de lui ajouter un indice (et de décaler les indices dans certains cas), mais guère plus. Pas besoin d'effectuer de multiplications, de divisions, ou d'autre opération plus complexe. Des décalages et des additions/soustractions suffisent. L'AGU est donc beaucoup plus simple qu'une ALU normale et se résume souvent à un vulgaire additionneur-soustracteur, éventuellement couplée à un décaleur pour multiplier les indices.
[[File:Unité d'accès mémoire avec unité de calcul dédiée.png|centre|vignette|upright=1.5|Unité d'accès mémoire avec unité de calcul dédiée]]
Le fait d'avoir une unité de calcul séparée pour les adresses peut s'expliquer pour plusieurs raisons. Sur les processeurs normaux, la raison est que cela simplifie un peu l'implémentation des modes d'adressage indirects. Sur les rares processeurs qui ont des registres séparés pour les adresses, un banc de registre dédié est réservé aux registres d'adresses, ce qui rend l'usage d'une unité de calcul d'adresse bien plus pratique. Une autre raison se manifestait sur les processeurs 8 bits : ils géraient des données de 8 bits, mais des adresses de 16 bits. Dans ce cas, le processeur avait une ALU simple de 16 bits pour les adresses, et une ALU complexe de 8 bits pour les données.
[[File:Unité d'accès mémoire avec registres d'adresse ou d'indice.png|centre|vignette|upright=2|Unité d'accès mémoire avec registres d'adresse ou d'indice]]
===La gestion de l'alignement et du boutisme===
L'interface mémoire gère les accès mémoire non-alignés, à cheval sur deux mots mémoire (rappelez-vous le chapitre sur l'alignement mémoire). Elle détecte les accès mémoire non-alignés et réagit en conséquence. Dans le cas où les accès non-alignés sont interdits, elle lève une exception matérielle. Dans le cas où ils sont autorisés, elle les gère automatiquement, à savoir qu'elle charge deux mots mémoire et les combine entre eux pour donner le résultat final. Dans les deux cas, cela demande d'ajouter des circuits de détection des accès non-alignés, et éventuellement des circuits pour le double lecture/écriture.
Les circuits de détection des accès non-alignés sont très simples. Dans le cas où les adresses sont alignées sur une puissance de deux (cas le plus courant), il suffit de vérifier les bits de poids faible de l'adresse à lire. Prenons l'exemple d'un processeur avec des adresses codées sur 64 bits, avec des mots mémoire de 32 bits, alignés sur 32 bits (4 octets). Un mot mémoire contient 4 octets, les contraintes d'alignement font que les adresses autorisées sont des multiples de 4. En conséquence, les 2 bits de poids faible d'une adresse valide sont censés être à 0. En vérifiant la valeur de ces deux bits, on détecte facilement les accès non-alignés.
En clair, détecter les accès non-alignés demande de tester si les bits de poids faibles adéquats sont à 0. Il suffit donc d'un circuit de comparaison avec zéro; qui est une simple porte OU. Cette porte OU génère un bit qui indique si l'accès testé est aligné ou non : 1 si l'accès est non-aligné, 0 sinon. Le signal peut être transmis au séquenceur pour générer une exception matérielle, ou utilisé dans l'unité d'accès mémoire pour la double lecture/écriture.
La gestion automatique des accès non-alignés est plus complexe. Dans ce cas, l'unité mémoire charge deux mots mémoire et les combine entre eux pour donner le résultat final. Charger deux mots mémoires consécutifs est assez simple, si le registre d'interfaçage est un compteur. L'accès initial charge le premier mot mémoire, puis l'adresse stockée dans le registre d'interfaçage est incrémentée pour démarrer un second accès. Le circuit pour combiner deux mots mémoire contient des registres, des circuits de décalage, des multiplexeurs.
===Le rafraichissement mémoire optimisé et le contrôleur mémoire intégré===
Depuis les années 80, les processeurs sont souvent combinés avec une mémoire principale de type DRAM. De telles mémoires doivent être rafraichies régulièrement pour ne pas perdre de données. Le rafraichissement se fait généralement adresse par adresse, ou ligne par ligne (les lignes sont des super-bytes internes à la DRAM). Le rafraichissement est en théorie géré par le contrôleur mémoire installé sur la carte mère. Mais au tout début de l'informatique, du temps des processeurs 8 bits, le rafraichissement mémoire était géré directement par le processeur.
Si quelques processeurs géraient le rafraichissement mémoire avec des interruptions, d'autres processeurs disposaient d’optimisations pour optimiser le rafraichissement mémoire. Divers processeurs implémentaient de quoi faciliter le rafraichissement par adresse. Par exemple, le processeur Zilog Z80 contenait un compteur de ligne, un registre qui contenait le numéro de la prochaine ligne à rafraichir. Il était incrémenté à chaque rafraichissement mémoire, automatiquement, par le processeur lui-même. Un ''timer'' interne permettait de savoir quand rafraichir la mémoire : quand ce ''timer'' atteignait 0, une commande de rafraichissement était envoyée à la mémoire, et le ''timer'' était ''reset''. Et tout cela était intégré à l'unité d'accès mémoire.
Depuis les années 2000, les processeurs modernes ont un contrôleur mémoire DRAM intégré directement dans le processeur. Ce qui fait qu'ils gèrent non seulement le rafraichissement, mais aussi d'autres fonctions bien pus complexes.
==Le chemin de données et son réseau d'interconnexions==
Nous venons de voir que le chemin de données contient une unité de calcul (parfois plusieurs), des registres isolés, un banc de registre, une unité mémoire. Le tout est chapeauté par une unité de contrôle qui commande le chemin de données, qui fera l'objet des prochains chapitres. Mais il faut maintenant relier registres, ALU et unité mémoire pour que l'ensemble fonctionne. Pour cela, diverses interconnexions internes au processeur se chargent de relier le tout.
Sur les anciens processeurs, les interconnexions sont assez simples et se résument à un ou deux '''bus internes au processeur''', reliés au bus mémoire. C'était la norme sur des architectures assez ancienne, qu'on n'a pas encore vu à ce point du cours, appelées les architectures à accumulateur et à pile. Mais ce n'est plus la solution utilisée actuellement. De nos jours, le réseaux d'interconnexion intra-processeur est un ensemble de connexions point à point entre ALU/registres/unité mémoire. Et paradoxalement, cela rend plus facile de comprendre ce réseau d'interconnexion.
===Introduction propédeutique : l'implémentation des modes d'adressage principaux===
L'organisation interne du processeur dépend fortement des modes d'adressage supportés. Pour simplifier les explications, nous allons séparer les modes d'adressage qui gèrent les pointeurs et les autres. Suivant que le processeur supporte les pointeurs ou non, l'organisation des bus interne est légèrement différente. La différence se voit sur les connexions avec le bus d'adresse et de données.
Tout processeur gère au minimum le '''mode d'adressage absolu''', où l'adresse est intégrée à l'instruction. Le séquenceur extrait l'adresse mémoire de l'instruction, et l'envoie sur le bus d'adresse. Pour cela, le séquenceur est relié au bus d'adresse, le chemin de donnée est relié au bus de données. Le chemin de donnée n'est pas connecté au bus d'adresse, il n'y a pas d'autres connexions.
[[File:Chemin de données sans support des pointeurs.png|centre|vignette|upright=2|Chemin de données sans support des pointeurs]]
Le '''support des pointeurs''' demande d'intégrer des modes d'adressage dédiés : l'adressage indirect à registre, l'adresse base + indice, et les autres. Les pointeurs sont stockés dans le banc de registre et sont modifiés par l'unité de calcul. Pour supporter les pointeurs, le chemin de données est connecté sur le bus d'adresse avec le séquenceur. Suivant le mode d'adressage, le bus d'adresse est relié soit au chemin de données, soit au séquenceur.
[[File:Chemin de données avec support des pointeurs.png|centre|vignette|upright=2|Chemin de données avec support des pointeurs]]
Pour terminer, il faut parler des instructions de '''copie mémoire vers mémoire''', qui copient une donnée d'une adresse mémoire vers une autre. Elles ne se passent pas vraiment dans le chemin de données, mais se passent purement au niveau des registres d’interfaçage. L'usage d'un registre d’interfaçage unique permet d'implémenter ces instructions très facilement. Elle se fait en deux étapes : on copie la donnée dans le registre d’interfaçage, on l'écrit en mémoire RAM. L'adresse envoyée sur le bus d'adresse n'est pas la même lors des deux étapes.
===Le banc de registre est multi-port, pour gérer nativement les opérations dyadiques===
Les architectures RISC et CISC incorporent un banc de registre, qui est connecté aux unités de calcul et au bus mémoire. Et ce banc de registre peut être mono-port ou multiport. S'il a existé d'anciennes architectures utilisant un banc de registre mono-port, elles sont actuellement obsolètes. Nous les aborderons dans un chapitre dédié aux architectures dites canoniques, mais nous pouvons les laisser de côté pour le moment. De nos jours, tous les processeurs utilisent un banc de registre multi-port.
[[File:Chemin de données minimal d'une architecture LOAD-STORE (sans MOV inter-registres).png|centre|vignette|upright=2|Chemin de données minimal d'une architecture LOAD-STORE (sans MOV inter-registres)]]
Le banc de registre multiport est optimisé pour les opérations dyadiques. Il dispose précisément de deux ports de lecture et d'un port d'écriture pour l'écriture. Un port de lecture par opérande et le port d'écriture pour enregistrer le résultat. En clair, le processeur peut lire deux opérandes et écrire un résultat en un seul cycle d'horloge. L'avantage est que les opérations simples ne nécessitent qu'une micro-opération, pas plus.
[[File:ALU data paths.svg|centre|vignette|upright=1.5|Processeur LOAD-STORE avec un banc de registre multiport, avec les trois ports mis en évidence.]]
===Une architecture LOAD-STORE basique, avec adressage absolu===
Voyons maintenant comment l'implémentation d'une architecture RISC très simple, qui ne supporte pas les adressages pour les pointeurs, juste les adressages inhérent (à registres) et absolu (par adresse mémoire). Les instructions LOAD et STORE utilisent l'adressage absolu, géré par le séquenceur, reste à gérer l'échange entre banc de registres et bus de données. Une lecture LOAD relie le bus de données au port d'écriture du banc de registres, alors que l'écriture relie le bus au port de lecture du banc de registre. Pour cela, il faut ajouter des multiplexeurs sur les chemins existants, comme illustré par le schéma ci-dessous.
[[File:Bus interne au processeur sur archi LOAD STORE avec banc de registres multiport.png|centre|vignette|upright=2|Organisation interne d'une architecture LOAD STORE avec banc de registres multiport. Nous n'avons pas représenté les signaux de commandes envoyés par le séquenceur au chemin de données.]]
Ajoutons ensuite les instructions de copie entre registres, souvent appelées instruction COPY ou MOV. Elles existent sur la plupart des architectures LOAD-STORE. Une première solution boucle l'entrée du banc de registres sur son entrée, ce qui ne sert que pour les copies de registres.
[[File:Chemin de données d'une architecture LOAD-STORE.png|centre|vignette|upright=2|Chemin de données d'une architecture LOAD-STORE]]
Mais il existe une seconde solution, qui ne demande pas de modifier le chemin de données. Il est possible de faire passer les copies de données entre registres par l'ALU. Lors de ces copies, l'ALU une opération ''Pass through'', à savoir qu'elle recopie une des opérandes sur sa sortie. Le fait qu'une ALU puisse effectuer une opération ''Pass through'' permet de fortement simplifier le chemin de donnée, dans le sens où cela permet d'économiser des multiplexeurs. Mais nous verrons cela sous peu. D'ailleurs, dans la suite du chapitre, nous allons partir du principe que les copies entre registres passent par l'ALU, afin de simplifier les schémas.
===L'ajout des modes d'adressage indirects à registre pour les pointeurs===
Passons maintenant à l'implémentation des modes d'adressages pour les pointeurs. Avec eux, l'adresse mémoire à lire/écrire n'est pas intégrée dans une instruction, mais est soit dans un registre, soit calculée par l'ALU.
Le premier mode d'adressage de ce type est le mode d'adressage indirect à registre, où l'adresse à lire/écrire est dans un registre. L'implémenter demande donc de connecter la sortie du banc de registres au bus d'adresse. Il suffit d'ajouter un MUX en sortie d'un port de lecture.
[[File:Chemin de données à trois bus.png|centre|vignette|upright=2|Chemin de données à trois bus.]]
Le mode d'adressage base + indice est un mode d'adressage où l'adresse à lire/écrire est calculée à partir d'une adresse et d'un indice, tous deux présents dans un registre. Le calcul de l'adresse implique au minimum une addition et donc l'ALU. Dans ce cas, on doit connecter la sortie de l'unité de calcul au bus d'adresse.
[[File:Bus avec adressage base+index.png|centre|vignette|upright=2|Bus avec adressage base+index]]
Le chemin de données précédent gère aussi le mode d'adressage indirect avec pré-décrément. Pour rappel, ce mode d'adressage est une variante du mode d'adressage indirect, qui utilise une pointeur/adresse stocké dans un registre. La différence est que ce pointeur est décrémenté avant d'être envoyé sur le bus d'adresse. L'implémentation matérielle est la même que pour le mode Base + Indice : l'adresse est lue depuis les registres, décrémentée dans l'ALU, et envoyée sur le bus d'adresse.
Le schéma précédent montre que le bus d'adresse est connecté à un MUX avant l'ALU et un autre MUX après. Mais il est possible de se passer du premier MUX, utilisé pour le mode d'adressage indirect à registre. La condition est que l'ALU supporte l'opération ''pass through'', un NOP, qui recopie une opérande sur sa sortie. L'ALU fera une opération NOP pour le mode d'adressage indirect à registre, un calcul d'adresse pour le mode d'adressage base + indice. Par contre, faire ainsi rendra l'adressage indirect légèrement plus lent, vu que le temps de passage dans l'ALU sera compté.
[[File:Bus avec adressage indirect.png|centre|vignette|upright=2|Bus avec adressages pour les pointeurs, simplifié.]]
Dans ce qui va suivre, nous allons partir du principe que le processeur est implémenté en suivant le schéma précédent, afin d'avoir des schéma plus lisibles.
===L'adressage immédiat et les modes d'adressages exotiques===
Passons maintenant au mode d’adressage immédiat, qui permet de préciser une constante dans une instruction directement. La constante est extraite de l'instruction par le séquenceur, puis insérée au bon endroit dans le chemin de données. Pour les opérations arithmétiques/logiques/branchements, il faut insérer la constante extraite sur l'entrée de l'ALU. Sur certains processeurs, la constante peut être négative et doit alors subir une extension de signe dans un circuit spécialisé.
[[File:Chemin de données - Adressage immédiat avec extension de signe.png|centre|vignette|upright=2|Chemin de données - Adressage immédiat avec extension de signe.]]
L'implémentation précédente gère aussi les modes d'adressage base + décalage et absolu indexé. Pour rappel, le premier ajoute une constante à une adresse prise dans les registres, le second prend une adresse constante et lui ajoute un indice pris dans les registres. Dans les deux cas, on lit un registre, extrait une constante/adresse de l’instruction, additionne les deux dans l'ALU, avant d'envoyer le résultat sur le bus d'adresse. La seule difficulté est de désactiver l'extension de signe pour les adresses.
Le mode d'adressage absolu peut être traité de la même manière, si l'ALU est capable de faire des NOPs. L'adresse est insérée au même endroit que pour le mode d'adressage immédiat, parcours l'unité de calcul inchangée parce que NOP, et termine sur le bus d'adresse.
[[File:Chemin de données avec une ALU capable de faire des NOP.png|centre|vignette|upright=2|Chemin de données avec adressage immédiat étendu pour gérer des adresses.]]
Passons maintenant au cas particulier d'une instruction MOV qui copie une constante dans un registre. Il n'y a rien à faire si l'unité de calcul est capable d'effectuer une opération NOP/''pass through''. Pour charger une constante dans un registre, l'ALU est configurée pour faire un NOP, la constante traverse l'ALU et se retrouve dans les registres. Si l'ALU ne gère pas les NOP, la constante doit être envoyée sur l'entrée d'écriture du banc de registres, à travers un MUX dédié.
[[File:Implémentation de l'adressage immédiat dans le chemin de données.png|centre|vignette|upright=2|Implémentation de l'adressage immédiat dans le chemin de données]]
===Les architectures CISC : les opérations ''load-op''===
Tout ce qu'on a vu précédemment porte sur les processeurs de type LOAD-STORE, souvent confondus avec les processeurs de type RISC, où les accès mémoire sont séparés des instructions utilisant l'ALU. Il est maintenant temps de voir les processeurs CISC, qui gèrent des instructions ''load-op'', qui peuvent lire une opérande depuis la mémoire.
L'implémentation des opérations ''load-op'' relie le bus de donnée directement sur une entrée de l'unité de calcul, en utilisant encore une fois un multiplexeur. L'implémentation parait simple, mais c'est parce que toute la complexité est déportée dans le séquenceur. C'est lui qui se charge de détecter quand la lecture de l'opérande est terminée, quand l'opérande est disponible.
Les instructions ''load-op'' s'exécutent en plusieurs étapes, en plusieurs micro-opérations. Il y a typiquement une étape pour l'opérande à lire en mémoire et une étape de calcul. L'usage d'un registre d’interfaçage permet d'implémenter les instructions ''load-op'' très facilement. Une opération ''load-op'' charge l'opérande en mémoire dans un registre d’interfaçage, puis relier ce registre d’interfaçage sur une des entrées de l'ALU. Un simple multiplexeur suffit pour implémenter le tout, en plus des modifications adéquates du séquenceur.
[[File:Chemin de données d'un CPU CISC avec lecture des opérandes en mémoire.png|centre|vignette|upright=2|Chemin de données d'un CPU CISC avec lecture des opérandes en mémoire]]
Supporter les instructions multi-accès (qui font plusieurs accès mémoire) ne modifie pas fondamentalement le réseau d'interconnexion, ni le chemin de données La raison est que supporter les instructions multi-accès se fait au niveau du séquenceur. En réalité, les accès mémoire se font en série, l'un après l'autre, sous la commande du séquenceur qui émet plusieurs micro-opérations mémoire consécutives. Les données lues sont placées dans des registres d’interactivement mémoire, ce qui demande d'ajouter des registres d’interfaçage mémoire en plus.
==Annexe : le cas particulier du pointeur de pile==
Le pointeur de pile est un registre un peu particulier. Il peut être placé dans le chemin de données ou dans le séquenceur, voire dans l'unité de chargement, tout dépend du processeur. Tout dépend de si le pointeur de pile gère une pile d'adresses de retour ou une pile d'appel.
===Le pointeur de pile non-adressable explicitement===
Avec une pile d'adresse de retour, le pointeur de pile n'est pas adressable explicitement, il est juste adressé implicitement par des instructions d'appel de fonction CALL et des instructions de retour de fonction RET. Le pointeur de pile est alors juste incrémenté ou décrémenté par un pas constant, il ne subit pas d'autres opérations, son adressage est implicite. Il est juste incrémenté/décrémenté par pas constants, qui sont fournis par le séquenceur. Il n'y a pas besoin de le relier au chemin de données, vu qu'il n'échange pas de données avec les autres registres. Il y a alors plusieurs solutions, mais la plus simple est de placer le pointeur de pile dans le séquenceur et de l'incrémenter par un incrémenteur dédié.
Quelques processeurs simples disposent d'une pile d'appel très limitée, où le pointeur de pile n'est pas adressable explicitement. Il est adressé implicitement par les instruction CALL, RET, mais aussi PUSH et POP, mais aucune autre instruction ne permet cela. Là encore, le pointeur de pile ne communique pas avec les autres registres. Il est juste incrémenté/décrémenté par pas constants, qui sont fournis par le séquenceur. Là encore, le plus simple est de placer le pointeur de pile dans le séquenceur et de l'incrémenter par un incrémenteur dédié.
Dans les deux cas, le pointeur de pile est placé dans l'unité de contrôle, le séquenceur, et est associé à un incrémenteur dédié. Il se trouve que cet incrémenteur est souvent partagé avec le ''program counter''. En effet, les deux sont des adresses mémoire, qui sont incrémentées et décrémentées par pas constants, ne subissent pas d'autres opérations (si ce n'est des branchements, mais passons). Les ressemblances sont suffisantes pour fusionner les deux circuits. Ils peuvent donc avoir un '''incrémenteur partagé'''.
L'incrémenteur en question est donc partagé entre pointeur de pile, ''program counter'' et quelques autres registres similaires. Par exemple, le Z80 intégrait un registre pour le rafraichissement mémoire, qui était réalisé par le CPU à l'époque. Ce registre contenait la prochaine adresse mémoire à rafraichir, et était incrémenté à chaque rafraichissement d'une adresse. Et il était lui aussi intégré au séquenceur et incrémenté par l'incrémenteur partagé.
[[File:Organisation interne d'une architecture à pile.png|centre|vignette|upright=2|Organisation interne d'une architecture à pile]]
===Le pointeur de pile adressable explicitement===
Maintenant, étudions le cas d'une pile d'appel, précisément d'une pile d'appel avec des cadres de pile de taille variable. Sous ces conditions, le pointeur de pile est un registre adressable, avec un nom/numéro de registre dédié. Tel est par exemple le cas des processeurs x86 avec le registre ESP (''Extended Stack Pointer''). 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 calculs d'adresse, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres.
Dans ce cas, la meilleure solution est de placer le pointeur de pile dans le banc de registre généraux, avec les autres registres entiers. En faisant cela, la manipulation du pointeur de pile est faite par l'unité de calcul entière, pas besoin d'utiliser un incrémenteur dédiée. Il a existé des processeurs qui mettaient le pointeur de pile dans le banc de registre, mais l'incrémentaient avec un incrémenteur dédié, mais nous les verrons dans le chapitre sur les architectures à accumulateur. La raison est que sur les processeurs concernés, les adresses ne faisaient pas la même taille que les données : c'était des processeurs 8 bits, qui géraient des adresses de 16 bits.
==Annexe : l'implémentation du système d'''aliasing'' des registres des CPU x86==
Il y a quelques chapitres, nous avions parlé du système d'''aliasing'' des registres des CPU x86. Pour rappel, il permet de donner plusieurs noms de registre pour un même registre. Plus précisément, pour un registre 64 bits, le registre complet aura un nom de registre, les 32 bits de poids faible auront leur nom de registre dédié, idem pour les 16 bits de poids faible, etc. Il est possible de faire des calculs sur ces moitiés/quarts/huitièmes de registres sans problème.
===L'''aliasing'' du 8086, pour les registres 16 bits===
[[File:Register 8086.PNG|vignette|Register 8086]]
L'implémentation de l'''aliasing'' est apparue sur les premiers CPU Intel 16 bits, notamment le 8086. En tout, ils avaient quatre registres généraux 16 bits : AX, BX, CX et DX. Ces quatre registres 16 bits étaient coupés en deux octets, chacun adressable. Par exemple, le registre AX était coupé en deux octets nommés AH et AL, chacun ayant son propre nom/numéro de registre. Les instructions d'addition/soustraction pouvaient manipuler le registre AL, ou le registre AH, ce qui modifiait les 8 bits de poids faible ou fort selon le registre choisit.
Le banc de registre ne gére que 4 registres de 16 bits, à savoir AX, BX, CX et DX. Lors d'une lecture d'un registre 8 bits, le registre 16 bit entier est lu depuis le banc de registre, mais les bits inutiles sont ignorés. Par contre, l'écriture peut se faire soit avec 16 bits d'un coup, soit pour seulement un octet. Le port d'écriture du banc de registre peut être configuré de manière à autoriser l'écriture soit sur les 16 bits du registre, soit seulement sur les 8 bits de poids faible, soit écrire dans les 8 bits de poids fort.
[[File:Port d'écriture du banc de registre du 8086.png|centre|vignette|upright=2.5|Port d'écriture du banc de registre du 8086]]
Une opération sur un registre 8 bits se passe comme suit. Premièrement, on lit le registre 16 bits complet depuis le banc de registre. Si l'on a sélectionné l'octet de poids faible, il ne se passe rien de particulier, l'opérande 16 bits est envoyée directement à l'ALU. Mais si on a sélectionné l'octet de poids fort, la valeur lue est décalée de 7 rangs pour atterrir dans les 8 octets de poids faible. Ensuite, l'unité de calcul fait un calcul avec cet opérande, un calcul 16 bits tout ce qu'il y a de plus classique. Troisièmement, le résultat est enregistré dans le banc de registre, en le configurant convenablement. La configuration précise s'il faut enregistrer le résultat dans un registre 16 bits, soit seulement dans l'octet de poids faible/fort.
Afin de simplifier le câblage, les 16 bits des registres AX/BX/CX/DX sont entrelacés d'une manière un peu particulière. Intuitivement, on s'attend à ce que les bits soient physiquement dans le même ordre que dans le registre : le bit 0 est placé à côté du bit 1, suivi par le bit 2, etc. Mais à la place, l'octet de poids fort et de poids faible sont mélangés. Deux bits consécutifs appartiennent à deux octets différents. Le tout est décrit dans le tableau ci-dessous.
{|class="wikitable"
|-
! Registre 16 bits normal
| class="f_bleu" | 15
| class="f_bleu" | 14
| class="f_bleu" | 13
| class="f_bleu" | 12
| class="f_bleu" | 11
| class="f_bleu" | 10
| class="f_bleu" | 9
| class="f_bleu" | 8
| class="f_rouge" | 7
| class="f_rouge" | 6
| class="f_rouge" | 5
| class="f_rouge" | 4
| class="f_rouge" | 3
| class="f_rouge" | 2
| class="f_rouge" | 1
| class="f_rouge" | 0
|-
! Registre 16 bits du 8086
| class="f_bleu" | 15
| class="f_rouge" | 7
| class="f_bleu" | 14
| class="f_rouge" | 6
| class="f_bleu" | 13
| class="f_rouge" | 5
| class="f_bleu" | 12
| class="f_rouge" | 4
| class="f_bleu" | 11
| class="f_rouge" | 3
| class="f_bleu" | 10
| class="f_rouge" | 2
| class="f_bleu" | 9
| class="f_rouge" | 1
| class="f_bleu" | 8
| class="f_rouge" | 0
|}
En faisant cela, le décaleur en entrée de l'ALU est bien plus simple. Il y a 8 multiplexeurs, mais le câblage est bien plus simple. Par contre, en sortie de l'ALU, il faut remettre les bits du résultat dans l'ordre adéquat, celui du registre 8086. Pour cela, les interconnexions sur le port d'écriture sont conçues pour. Il faut juste mettre les fils de sortie de l'ALU sur la bonne entrée, par besoin de multiplexeurs.
===L'''aliasing'' sur les processeurs x86 32/64 bits===
Les processeurs x86 32 et 64 bits ont un système d'''aliasing'' qui complète le système précédent. Les processeurs 32 bits étendent les registres 16 bits existants à 32 bits. Pour ce faire, le registre 32 bit a un nouveau nom de registre, distincts du nom de registre utilisé pour l'ancien registre 16 bits. Il est possible d'adresser les 16 bits de poids faible de ce registre, avec le même nom de registre que celui utilisé pour le registre 16 sur les processeurs d'avant. Même chose avec les processeurs 64, avec l'ajout d'un nouveau nom de registre pour adresser un registre de 64 bit complet.
En soit, implémenter ce système n'est pas compliqué. Prenons le cas du registre RAX (64 bits), et de ses subdivisions nommées EAX (32 bits), AX (16 bits). À l'intérieur du banc de registre, il n'y a que le registre RAX. Le banc de registre ne comprend qu'un seul nom de registre : RAX. Les subdivisions EAX et AX n'existent qu'au niveau de l'écriture dans le banc de registre. L'écriture dans le banc de registre est configurable, de manière à ne modifier que les bits adéquats. Le résultat d'un calcul de l'ALU fait 64 bits, il est envoyé sur le port d'écriture. À ce niveau, soit les 64 bits sont écrits dans le registre, soit seulement les 32/16 bits de poids faible. Le système du 8086 est préservé pour les écritures dans les 16 bits de poids faible.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les composants d'un processeur
| prevText=Les composants d'un processeur
| next=L'unité de chargement et le program counter
| nextText=L'unité de chargement et le program counter
}}
</noinclude>
k7ub5q2hd00z28xvqefx6d6qxe7r3pk
745836
745834
2025-07-02T20:42:43Z
Mewtow
31375
/* Les unités de calcul spécialisées */
745836
wikitext
text/x-wiki
Comme vu précédemment, le '''chemin de donnée''' est l'ensemble des composants dans lesquels circulent les données dans le processeur. Il comprend l'unité de calcul, les registres, l'unité de communication avec la mémoire, et le ou les interconnexions qui permettent à tout ce petit monde de communiquer. Dans ce chapitre, nous allons voir ces composants en détail.
==Les unités de calcul==
Le processeur contient des circuits capables de faire des calculs arithmétiques, des opérations logiques, et des comparaisons, qui sont regroupés dans une unité de calcul appelée '''unité arithmétique et logique'''. Certains préfèrent l’appellation anglaise ''arithmetic and logic unit'', ou ALU. Par défaut, ce terme est réservé aux unités de calcul qui manipulent des nombres entiers. Les unités de calcul spécialisées pour les calculs flottants sont désignées par le terme "unité de calcul flottant", ou encore FPU (''Floating Point Unit'').
L'interface d'une unité de calcul est assez simple : on a des entrées pour les opérandes et une sortie pour le résultat du calcul. De plus, les instructions de comparaisons ou de calcul peuvent mettre à jour le registre d'état, qui est relié à une autre sortie de l’unité de calcul. Une autre entrée, l''''entrée de sélection de l'instruction''', spécifie l'opération à effectuer. Elle sert à configurer l'unité de calcul pour faire une addition et pas une multiplication, par exemple. Sur cette entrée, on envoie un numéro qui précise l'opération à effectuer. La correspondance entre ce numéro et l'opération à exécuter dépend de l'unité de calcul. Sur les processeurs où l'encodage des instructions est "simple", une partie de l'opcode de l'instruction est envoyé sur cette entrée.
[[File:Unité de calcul usuelle.png|centre|vignette|upright=2|Unité de calcul usuelle.]]
Il faut signaler que les processeurs modernes possèdent plusieurs unités de calcul, toutes reliées aux registres. Cela permet d’exécuter plusieurs calculs en même temps dans des unités de calcul différentes, afin d'augmenter les performances du processeur. Diverses technologies, abordées dans la suite du cours permettent de profiter au mieux de ces unités de calcul : pipeline, exécution dans le désordre, exécution superscalaire, jeux d'instructions VLIW, etc. Mais laissons cela de côté pour le moment.
===L'ALU entière : additions, soustractions, opérations bit à bit===
Un processeur contient plusieurs ALUs spécialisées. La principale, présente sur tous les processeurs, est l''''ALU entière'''. Elle s'occupe uniquement des opérations sur des nombres entiers, les nombres flottants sont gérés par une ALU à part. Elle gère des opérations simples : additions, soustractions, opérations bit à bit, parfois des décalages/rotations. Par contre, elle ne gère pas la multiplication et la division, qui sont prises en charge par un circuit multiplieur/diviseur à part.
L'ALU entière a déjà été vue dans un chapitre antérieur, nommé "Les unités arithmétiques et logiques entières (simples)", qui expliquait comment en concevoir une. Nous avions vu qu'une ALU entière est une sorte de circuit additionneur-soustracteur amélioré, ce qui explique qu'elle gère des opérations entières simples, mais pas la multiplication ni la division. Nous ne reviendrons pas dessus. Cependant, il y a des choses à dire sur leur intégration au processeur.
Une ALU entière gère souvent une opération particulière, qui ne fait rien et recopie simplement une de ses opérandes sur sa sortie. L'opération en question est appelée l''''opération ''Pass through''''', encore appelée opération NOP. Elle est implémentée en utilisant un simple multiplexeur, placé en sortie de l'ALU. Le fait qu'une ALU puisse effectuer une opération ''Pass through'' permet de fortement simplifier le chemin de donnée, d'économiser des multiplexeurs. Mais nous verrons cela sous peu.
[[File:ALU avec opération NOP.png|centre|vignette|upright=2|ALU avec opération NOP.]]
Avant l'invention du microprocesseur, le processeur n'était pas un circuit intégré unique. L'ALU, le séquenceur et les registres étaient dans des puces séparées. Les ALU étaient vendues séparément et manipulaient des opérandes de 4/8 bits, les ALU 4 bits étaient très fréquentes. Si on voulait créer une ALU pour des opérandes plus grandes, il fallait construire l'ALU en combinant plusieurs ALU 4/8 bits. Par exemple, l'ALU des processeurs AMD Am2900 est une ALU de 16 bits composée de plusieurs sous-ALU de 4 bits. Cette technique qui consiste à créer des unités de calcul à partir d'unités de calcul plus élémentaires s'appelle en jargon technique du '''bit slicing'''. Nous en avions parlé dans le chapitre sur les unités de calcul, aussi nous n'en reparlerons pas plus ici.
L'ALU manipule des opérandes codées sur un certain nombre de bits. Par exemple, une ALU peut manipuler des entiers codés sur 8 bits, sur 16 bits, etc. En général, la taille des opérandes de l'ALU est la même que la taille des registres. Un processeur 32 bits, avec des registres de 32 bit, a une ALU de 32 bits. C'est intuitif, et cela rend l'implémentation du processeur bien plus facile. Mais il y a quelques exceptions, où l'ALU manipule des opérandes plus petits que la taille des registres. Par exemple, de nombreux processeurs 16 bits, avec des registres de 16 bits, utilisent une ALU de 8 bits. Un autre exemple assez connu est celui du Motorola 68000, qui était un processeur 32 bits, mais dont l'ALU faisait juste 16 bits. Son successeur, le 68020, avait lui une ALU de 32 bits.
Sur de tels processeurs, les calculs sont fait en plusieurs passes. Par exemple, avec une ALU 8 bit, les opérations sur des opérandes 8 bits se font en un cycle d'horloge, celles sur 16 bits se font en deux cycles, celles en 32 en quatre, etc. Si un programme manipule assez peu d'opérandes 16/32/64 bits, la perte de performance est assez faible. Diverses techniques visent à améliorer les performances, mais elles ne font pas de miracles. Par exemple, vu que l'ALU est plus courte, il est possible de la faire fonctionner à plus haute fréquence, pour réduire la perte de performance.
Pour comprendre comme est implémenté ce système de passes, prenons l'exemple du processeur 8 bit Z80. Ses registres entiers étaient des registres de 8 bits, alors que l'ALU était de 4 bits. Les calculs étaient faits en deux phases : une qui traite les 4 bits de poids faible, une autre qui traite les 4 bits de poids fort. Pour cela, les opérandes étaient placées dans des registres de 4 bits en entrée de l'ALU, plusieurs multiplexeurs sélectionnaient les 4 bits adéquats, le résultat était mémorisé dans un registre de résultat de 8 bits, un démultiplexeur plaçait les 4 bits du résultat au bon endroit dans ce registre. L'unité de contrôle s'occupait de la commande des multiplexeurs/démultiplexeurs. Les autres processeurs 8 ou 16 bits utilisent des circuits similaires pour faire leurs calculs en plusieurs fois.
[[File:ALU du Z80.png|centre|vignette|upright=2|ALU du Z80]]
Un exemple extrême est celui des des '''processeurs sériels''' (sous-entendu ''bit-sériels''), qui utilisent une '''ALU sérielle''', qui fait leurs calculs bit par bit, un bit à la fois. S'il a existé des processeurs de 1 bit, comme le Motorola MC14500B, la majeure partie des processeurs sériels étaient des processeurs 4, 8 ou 16 bits. L'avantage de ces ALU est qu'elles utilisent peu de transistors, au détriment des performances par rapport aux processeurs non-sériels. Mais un autre avantage est qu'elles peuvent gérer des opérandes de grande taille, avec plus d'une trentaine de bits, sans trop de problèmes.
===Les circuits multiplieurs et diviseurs===
Les processeurs modernes ont une ALU pour les opérations simples (additions, décalages, opérations logiques), couplée à une ALU pour les multiplications, un circuit multiplieur séparé. Précisons qu'il ne sert pas à grand chose de fusionner le circuit multiplieur avec l'ALU, mieux vaut les garder séparés par simplicité. Les processeurs haute performance disposent systématiquement d'un circuit multiplieur et gèrent la multiplication dans leur jeu d'instruction.
Le cas de la division est plus compliqué. La présence d'un circuit multiplieur est commune, mais les circuits diviseurs sont eux très rares. Leur cout en circuit est globalement le même que pour un circuit multiplieur, mais le gain en performance est plus faible. Le gain en performance pour la multiplication est modéré car il s'agit d'une opération très fréquente, alors qu'il est très faible pour la division car celle-ci est beaucoup moins fréquente.
Pour réduire le cout en circuits, il arrive que l'ALU pour les multiplications gère à la fois la multiplication et la division. Les circuits multiplieurs et diviseurs sont en effet très similaires et partagent beaucoup de points communs. Généralement, la fusion se fait pour les multiplieurs/diviseurs itératifs.
Il existe cependant des circuits qui se passent de multiplieurs, tout en supportant la multiplication dans leur jeu d'instruction. Certains utilisent pour cela du microcode, technique qu'on verra dans deux chapitres, mais l'Intel Atom utilise une technique franchement peu ordinaire. L'Intel Atom utilise l'unité de calcul flottante pour faire les multiplications entières. Les opérandes entières sont traduites en nombres flottants, multipliés par l'unité de calcul flottante, puis le résultat est converti en un entier avec quelques corrections à la clé. Ainsi, on fait des économies de circuits, en mutualisant le multiplieur entre l'unité de calcul flottante et l'ALU entière, surtout que ce multiplieur manipule des opérandes plus courtes. Les performances sont cependant réduites comparé à l'usage d'un vrai multiplieur entier.
===Le ''barrel shifter''===
On vient d'expliquer que la présence de plusieurs ALU spécialisée est très utile pour implémenter des opérations compliquées à insérer dans une unité de calcul normale, comme la multiplication et la division. Mais les décalages sont aussi dans ce cas, de même que les rotations. Nous avions vu il y a quelques chapitres qu'ils sont réalisés par un circuit spécialisé, appelé un ''barrel shifter'', qu'il est difficile de fusionner avec une ALU normale. Aussi, beaucoup de processeurs incorporent un ''barrel shifter'' séparé de l'ALU.
Les processeurs ARM utilise un ''barrel shifter'', mais d'une manière un peu spéciale. On a vu il y a quelques chapitres que si on fait une opération logique, une addition, une soustraction ou une comparaison, la seconde opérande peut être décalée automatiquement. L'instruction incorpore le type de de décalage à faire et par combien de rangs il faut décaler directement à côté de l'opcode. Cela simplifie grandement les calculs d'adresse, qui se font en une seule instruction, contre deux ou trois sur d'autres architectures. Et pour cela, l'ALU proprement dite est précédée par un ''barrel shifter'',une seconde ALU spécialisée dans les décalages. Notons que les instructions MOV font aussi partie des instructions où la seconde opérande (le registre source) peut être décalé : cela signifie que les MOV passent par l'ALU, qui effectue alors un NOP, une opération logique OUI.
===Les unités de calcul spécialisées===
Un processeur peut disposer d’unités de calcul séparées de l'unité de calcul principale, spécialisées dans les décalages, les divisions, etc. Et certaines d'entre elles sont spécialisées dans des opérations spécifiques, qui ne sont techniquement pas des opérations entières, sur des nombres entiers.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Depuis les années 90-2000, presque tous les processeurs utilisent une unité de calcul spécialisée pour les nombres flottants : la '''Floating-Point Unit''', aussi appelée FPU. En général, elle regroupe un additionneur-soustracteur flottant et un multiplieur flottant. Parfois, elle incorpore un diviseur flottant, tout dépend du processeur. Précisons que sur certains processeurs, la FPU et l'ALU entière ne vont pas à la même fréquence, pour des raisons de performance et de consommation d'énergie !
La FPU intègre un circuit multiplieur entier, utilisé pour les multiplications flottantes, afin de multiplier les mantisses entre elles. Quelques processeurs utilisaient ce multiplieur pour faire les multiplications entières. En clair, au lieu d'avoir un multiplieur entier séparé du multiplieur flottant, les deux sont fusionnés en un seul circuit. Il s'agit d'une optimisation qui a été utilisée sur quelques processeurs 32 bits, qui supportaient les flottants 64 bits (double précision). Les processeurs Atom étaient dans ce cas, idem pour l'Athlon première génération. Les processeurs modernes n'utilisent pas cette optimisation pour des raisons qu'on ne peut pas expliquer ici (réduction des dépendances structurelles, émission multiple).
Il existe des unités de calcul spécialisées pour les calculs d'adresse. Elles ne supportent guère plus que des incrémentations/décrémentations, des additions/soustractions, et des décalages simples. Les autres opérations n'ont pas de sens avec des adresses. L'usage d'ALU spécialisées pour les adresses est un avantage sur les processeurs où les adresses ont une taille différente des données, ce qui est fréquent sur les anciens processeurs 8 bits.
De nombreux processeurs modernes disposent d'une unité de calcul spécialisée dans le calcul des conditions, tests et branchements. C’est notamment le cas sur les processeurs sans registre d'état, qui disposent de registres à prédicats. En général, les registres à prédicats sont placés à part des autres registres, dans un banc de registre séparé. L'unité de calcul normale n'est pas reliée aux registres à prédicats, alors que l'unité de calcul pour les branchements/test/conditions l'est. Les registres à prédicats sont situés juste en sortie de cette unité de calcul.
==Les registres du processeur==
Après avoir vu l'unité de calcul, il est temps de passer aux registres d'un processeur. L'organisation des registres est généralement assez compliquée, avec quelques registres séparés des autres comme le registre d'état ou le ''program counter''. Les registres d'un processeur peuvent se classer en deux camps : soit ce sont des registres isolés, soit ils sont regroupés en paquets appelés banc de registres.
Un '''banc de registres''' (''register file'') est une RAM, dont chaque byte est un registre. Il regroupe un paquet de registres différents dans un seul composant, dans une seule mémoire. Dans processeur moderne, on trouve un ou plusieurs bancs de registres. La répartition des registres, à savoir quels registres sont dans le banc de registre et quels sont ceux isolés, est très variable suivant les processeurs.
[[File:Register File Simple.svg|centre|vignette|upright=1|Banc de registres simplifié.]]
===L'adressage du banc de registres===
Le banc de registre est une mémoire comme une autre, avec une entrée d'adresse qui permet de sélectionner le registre voulu. Plutot que d'adresse, nous allons parler d''''identifiant de registre'''. Le séquenceur forge l'identifiant de registre en fonction des registres sélectionnés. Dans les chapitres précédents, nous avions vu qu'il existe plusieurs méthodes pour sélectionner un registre, qui portent les noms de modes d'adressage. Et bien les modes d'adressage jouent un grand rôle dans la forge de l'identifiant de registre.
Pour rappel, sur la quasi-totalité des processeurs actuels, les registres généraux sont identifiés par un nom de registre, terme trompeur vu que ce nom est en réalité un numéro. En clair, les processeurs numérotent les registres, le numéro/nom du registre permettant de l'identifier. Par exemple, si je veux faire une addition, je dois préciser les deux registres pour les opérandes, et éventuellement le registre pour le résultat : et bien ces registres seront identifiés par un numéro. Mais tous les registres ne sont pas numérotés et ceux qui ne le sont pas sont adressés implicitement. Par exemple, le pointeur de pile sera modifié par les instructions qui manipulent la pile, sans que cela aie besoin d'être précisé par un nom de registre dans l'instruction.
Dans le cas le plus simple, les registres nommés vont dans le banc de registres, les registres adressés implicitement sont en-dehors, dans des registres isolés. L'idéntifiant de registre est alors simplement le nom de registre, le numéro. Le séquenceur extrait ce nom de registre de l'insutrction, avant de l'envoyer sur l'entrée d'adresse du banc de registre.
[[File:Adressage du banc de registres généruax.png|centre|vignette|upright=2|Adressage du banc de registres généraux]]
Dans un cas plus complexe, des registres non-nommés sont placés dans le banc de registres. Par exemple, les pointeurs de pile sont souvent placés dans le banc de registre, même s'ils sont adressés implicitement. Même des registres aussi importants que le ''program counter'' peuvent se mettre dans le banc de registre ! Nous verrons le cas du ''program counter'' dans le chapitre suivant, qui porte sur l'unité de chargement. Dans ce cas, le séquenceur forge l'identifiant de registre de lui-même. Dans le cas des registres nommés, il ajoute quelques bits aux noms de registres. Pour les registres adressés implicitement, il forge l'identifiant à partir de rien.
[[File:Adressage du banc de registre - cas général.png|centre|vignette|upright=2|Adressage du banc de registre - cas général]]
Nous verrons plus bas que dans certains cas, le nom de registre ne suffit pas à adresser un registre dans un banc de registre. Dans ce cas, le séquenceur rajoute des bits, comme dans l'exemple précédent. Tout ce qu'il faut retenir est que l'identifiant de registre est forgé par le séquenceur, qui se base entre autres sur le nom de registre s'il est présent, sur l'instruction exécutée dans le cas d'un registre adressé implicitement.
===Les registres généraux===
Pour rappel, les registres généraux peuvent mémoriser des entiers, des adresses, ou toute autre donnée codée en binaire. Ils sont souvent séparés des registres flottants sur les architectures modernes. Les registres généraux sont rassemblés dans un banc de registre dédié, appelé le '''banc de registres généraux'''. Le banc de registres généraux est une mémoire multiport, avec au moins un port d'écriture et deux ports de lecture. La raison est que les instructions lisent deux opérandes dans les registres et enregistrent leur résultat dans des registres. Le tout se marie bien avec un banc de registre à deux de lecture (pour les opérandes) et un d'écriture (pour le résultat).
[[File:Banc de registre multiports.png|centre|vignette|upright=2|Banc de registre multiports.]]
L'interface exacte dépend de si l'architecture est une architecture 2 ou 3 adresses. Pour rappel, la différence entre les deux tient dans la manière dont on précise le registre où enregistrer le résultat d'une opération. Avec les architectures 2-adresses, on précise deux registres : le premier sert à la fois comme opérande et pour mémoriser le résultat, l'autre sert uniquement d'opérande. Un des registres est donc écrasé pour enregistrer le résultat. Sur les architecture 3-adresses, on précise trois registres : deux pour les opérandes, un pour le résultat.
Les architectures 2-adresses ont un banc de registre où on doit préciser deux "adresses", deux noms de registre. L'interface du banc de registre est donc la suivante :
[[File:Register File Medium.svg|centre|vignette|upright=1.5|Register File d'une architecture à 2-adresses]]
Les architectures 3-adresses doivent rajouter une troisième entrée pour préciser un troisième nom de registre. L'interface du banc de registre est donc la suivante :
[[File:Register File Large.svg|centre|vignette|upright=1.5|Register File d'une architecture à 3-adresses]]
Rien n'empêche d'utiliser plusieurs bancs de registres sur un processeur qui utilise des registres généraux. La raison est une question d'optimisation. Au-delà d'un certain nombre de registres, il devient difficile d'utiliser un seul gros banc de registres. Il faut alors scinder le banc de registres en plusieurs bancs de registres séparés. Le problème est qu'il faut prévoir de quoi échanger des données entre les bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue du langage machine. Sur d'autres processeurs, les transferts de données entre bancs de registres se font via une instruction spéciale, souvent appelée COPY.
===Les registres flottants : banc de registre séparé ou unifié===
Passons maintenant aux registres flottants. Intuitivement, on a des registres séparés pour les entiers et les flottants. Il est alors plus simple d'utiliser un banc de registres séparé pour les nombres flottants, à côté d'un banc de registre entiers. L'avantage est que les nombres flottants et entiers n'ont pas forcément la même taille, ce qui se marie bien avec deux bancs de registres, où la taille des registres est différente dans les deux bancs.
Mais d'autres processeurs utilisent un seul '''banc de registres unifié''', qui regroupe tous les registres de données, qu'ils soient entier ou flottants. Par exemple, c'est le cas des Pentium Pro, Pentium II, Pentium III, ou des Pentium M : ces processeurs ont des registres séparés pour les flottants et les entiers, mais ils sont regroupés dans un seul banc de registres. Avec cette organisation, un registre flottant et un registre entier peuvent avoir le même nom de registre en langage machine, mais l'adresse envoyée au banc de registres ne doit pas être la même : le séquenceur ajoute des bits au nom de registre pour former l'adresse finale.
[[File:Désambiguïsation de registres sur un banc de registres unifié.png|centre|vignette|upright=2|Désambiguïsation de registres sur un banc de registres unifié.]]
===Le registre d'état===
Le registre d'état fait souvent bande à part et n'est pas placé dans un banc de registres. En effet, le registre d'état est très lié à l'unité de calcul. Il reçoit des indicateurs/''flags'' provenant de la sortie de l'unité de calcul, et met ceux-ci à disposition du reste du processeur. Son entrée est connectée à l'unité de calcul, sa sortie est reliée au séquenceur et/ou au bus interne au processeur.
Le registre d'état est relié au séquenceur afin que celui-ci puisse gérer les instructions de branchement, qui ont parfois besoin de connaitre certains bits du registre d'état pour savoir si une condition a été remplie ou non. D'autres processeurs relient aussi le registre d'état au bus interne, ce qui permet de lire son contenu et de le copier dans un registre de données. Cela permet d'implémenter certaines instructions, notamment celles qui permettent de mémoriser le registre d'état dans un registre général.
[[File:Place du registre d'état dans le chemin de données.png|centre|vignette|upright=2|Place du registre d'état dans le chemin de données]]
L'ALU fournit une sortie différente pour chaque bit du registre d'état, la connexion du registre d'état est directe, comme indiqué dans le schéma suivant. Vous remarquerez que le bit de retenue est à la fois connecté à la sortie de l'ALU, mais aussi sur son entrée. Ainsi, le bit de retenue calculé par une opération peut être utilisé pour la suivante. Sans cela, diverses instructions comme les opérations ''add with carry'' ne seraient pas possibles.
[[File:AluStatusRegister.svg|centre|vignette|upright=2|Registre d'état et unit de calcul.]]
Il est techniquement possible de mettre le registre d'état dans le banc de registre, pour économiser un registre. La principale difficulté est que les instructions doivent faire deux écritures dans le banc de registre : une pour le registre de destination, une pour le registre d'état. Soit on utilise deux ports d'écriture, soit on fait les deux écritures l'une après l'autre. Dans les deux cas, le cout en performances et en transistors n'en vaut pas le cout. D'ailleurs, je ne connais aucun processeur qui utilise cette technique.
Il faut noter que le registre d'état n'existe pas forcément en tant que tel dans le processeur. Quelques processeurs, dont le 8086 d'Intel, utilisent des bascules dispersées dans le processeur au lieu d'un vrai registre d'état. Les bascules dispersées mémorisent chacune un bit du registre d'état et sont placées là où elles sont le plus utile. Les bascules utilisées pour les branchements sont proches du séquenceur, le bascules pour les bits de retenue sont placées proche de l'ALU, etc.
===Les registres à prédicats===
Les registres à prédicats remplacent le registre d'état sur certains processeurs. Pour rappel, les registres à prédicat sont des registres de 1 bit qui mémorisent les résultats des comparaisons et instructions de test. Ils sont nommés/numérotés, mais les numéros en question sont distincts de ceux utilisés pour les registres généraux.
Ils sont placés à part, dans un banc de registres séparé. Le banc de registres à prédicats a une entrée de 1 bit connectée à l'ALU et une sortie de un bit connectée au séquenceur. Le banc de registres à prédicats est parfois relié à une unité de calcul spécialisée dans les conditions/instructions de test. Pour rappel, certaines instructions permettent de faire un ET, un OU, un XOR entre deux registres à prédicats. Pour cela, l'unité de calcul dédiée aux conditions peut lire les registres à prédicats, pour combiner le contenu de plusieurs d'entre eux.
[[File:Banc de registre pour les registres à prédicats.png|centre|vignette|upright=2|Banc de registre pour les registres à prédicats]]
===Les registres dédiés aux interruptions===
Dans le chapitre sur les registres, nous avions vu que certains processeurs dupliquaient leurs registres architecturaux, pour accélérer les interruptions ou les appels de fonction. Dans le cas qui va nous intéresser, les interruptions avaient accès à leurs propres registres, séparés des registres architecturaux. Les processeurs de ce type ont deux ensembles de registres identiques : un dédié aux interruptions, un autre pour les programmes normaux. Les registres dans les deux ensembles ont les mêmes noms, mais le processeur choisit le bon ensemble suivant s'il est dans une interruption ou non. Si on peut utiliser deux bancs de registres séparés, il est aussi possible d'utiliser un banc de registre unifié pour les deux.
Sur certains processeurs, le banc de registre est dupliqué en plusieurs exemplaires. La technique est utilisée pour les interruptions. Certains processeurs ont deux ensembles de registres identiques : un dédié aux interruptions, un autre pour les programmes normaux. Les registres dans les deux ensembles ont les mêmes noms, mais le processeur choisit le bon ensemble suivant s'il est dans une interruption ou non. On peut utiliser deux bancs de registres séparés, un pour les interruptions, et un pour les programmes.
Sur d'autres processeurs, on utilise un banc de registre unifié pour les deux ensembles de registres. Les registres pour les interruptions sont dans les adresses hautes, les registres pour les programmes dans les adresses basses. Le choix entre les deux est réalisé par un bit qui indique si on est dans une interruption ou non, disponible dans une bascule du processeur. Appelons là la bascule I.
===Le fenêtrage de registres===
[[File:Fenetre de registres.png|vignette|upright=1|Fenêtre de registres.]]
Le '''fenêtrage de registres''' fait que chaque fonction a accès à son propre ensemble de registres, sa propre fenêtre de registres. Là encore, cette technique duplique chaque registre architectural en plusieurs exemplaires qui portent le même nom. Chaque ensemble de registres architecturaux forme une fenêtre de registre, qui contient autant de registres qu'il y a de registres architecturaux. Lorsqu'une fonction s’exécute, elle se réserve une fenêtre inutilisée, et peut utiliser les registres de la fenêtre comme bon lui semble : une fonction manipule le registre architectural de la fenêtre réservée, mais pas les registres avec le même nom dans les autres fenêtres.
Il peut s'implémenter soit avec un banc de registres unifié, soit avec un banc de registre par fenêtre de registres.
Il est possible d'utiliser des bancs de registres dupliqués pour le fenêtrage de registres. Chaque fenêtre de registre a son propre banc de registres. Le choix entre le banc de registre à utiliser est fait par un registre qui mémorise le numéro de la fenêtre en cours. Ce registre commande un multiplexeur qui permet de choisir le banc de registre adéquat.
[[File:Fenêtrage de registres au niveau du banc de registres.png|vignette|Fenêtrage de registres au niveau du banc de registres.]]
L'utilisation d'un banc de registres unifié permet d'implémenter facilement le fenêtrage de registres. Il suffit pour cela de regrouper tous les registres des différentes fenêtres dans un seul banc de registres. Il suffit de faire comme vu au-dessus : rajouter des bits au nom de registre pour faire la différence entre les fenêtres. Cela implique de se souvenir dans quelle fenêtre de registre on est actuellement, cette information étant mémorisée dans un registre qui stocke le numéro de la fenêtre courante. Pour changer de fenêtre, il suffit de modifier le contenu de ce registre lors d'un appel ou retour de fonction avec un petit circuit combinatoire. Bien sûr, il faut aussi prendre en compte le cas où ce registre déborde, ce qui demande d'ajouter des circuits pour gérer la situation.
[[File:Désambiguïsation des fenêtres de registres.png|centre|vignette|upright=2|Désambiguïsation des fenêtres de registres.]]
==L'interface de communication avec la mémoire==
L''''interface avec la mémoire''' est, comme son nom l'indique, des circuits qui servent d'intermédiaire entre le bus mémoire et le processeur. Elle est parfois appelée l'unité mémoire, l'unité d'accès mémoire, la ''load-store unit'', et j'en oublie.
[[File:Unité de communication avec la mémoire, de type simple port.png|centre|vignette|upright=2|Unité de communication avec la mémoire, de type simple port.]]
Sur certains processeurs, elle gère les mémoires multiport.
[[File:Unité de communication avec la mémoire, de type multiport.png|centre|vignette|upright=2|Unité de communication avec la mémoire, de type multiport.]]
===Les registres d'interfaçage mémoire===
L'interface mémoire se résume le plus souvent à des '''registres d’interfaçage mémoire''', intercalés entre le bus mémoire et le chemin de données. Généralement, il y a au moins deux registres d’interfaçage mémoire : un registre relié au bus d'adresse, et autre relié au bus de données.
[[File:Registres d’interfaçage mémoire.png|centre|vignette|upright=2|Registres d’interfaçage mémoire.]]
Au lieu de lire ou écrire directement sur le bus, le processeur lit ou écrit dans ces registres, alors que l'unité d'accès mémoire s'occupe des échanges entre registres et bus mémoire. Lors d'une écriture, le processeur place l'adresse dans le registre d'interfaçage d'adresse, met la donnée à écrire dans le registre d'interfaçage de donnée, puis laisse l'unité d'accès mémoire faire son travail. Lors d'une lecture, il place l'adresse à lire sur le registre d'interfaçage d'adresse, il attend que la donnée soit lue, puis récupère la donnée dans le registre d'interfaçage de données.
L'avantage est que le processeur n'a pas à maintenir une donnée/adresse sur le bus durant tout un accès mémoire. Par exemple, prenons le cas où la mémoire met 15 cycles processeurs pour faire une lecture ou une écriture. Sans registres d'interfaçage mémoire, le processeur doit maintenir l'adresse durant 15 cycles, et aussi la donnée dans le cas d'une écriture. Avec ces registres, le processeur écrit dans les registres d'interfaçage mémoire au premier cycle, et passe les 14 cycles suivants à faire quelque chose d'autre. Par exemple, il faut faire un calcul en parallèle, envoyer des signaux de commande au banc de registre pour qu'il soit prêt une fois la donnée lue arrivée, etc. Cet avantage simplifie l'implémentation de certains modes d'adressage, comme on le verra à la fin du chapitre.
===L'unité de calcul d'adresse===
Les registres d'interfaçage sont presque toujours présents, mais le circuit que nous allons voir est complétement facultatif. Il s'agit d'une unité de calcul spécialisée dans les calculs d'adresse, dont nous avons parlé rapidement dans la section sur les ALU. Elle s'appelle l''''''Address generation unit''''', ou AGU. Elle est parfois séparée de l'interface mémoire proprement dit, et est alors considérée comme une unité de calcul à part, mais elle est généralement intimement liée à l'interface mémoire.
Elle sert pour certains modes d'adressage, qui demandent de combiner une adresse avec soit un indice, soit un décalage, plus rarement les deux. Les calculs d'adresse demandent de simplement incrémenter/décrémenter une adresse, de lui ajouter un indice (et de décaler les indices dans certains cas), mais guère plus. Pas besoin d'effectuer de multiplications, de divisions, ou d'autre opération plus complexe. Des décalages et des additions/soustractions suffisent. L'AGU est donc beaucoup plus simple qu'une ALU normale et se résume souvent à un vulgaire additionneur-soustracteur, éventuellement couplée à un décaleur pour multiplier les indices.
[[File:Unité d'accès mémoire avec unité de calcul dédiée.png|centre|vignette|upright=1.5|Unité d'accès mémoire avec unité de calcul dédiée]]
Le fait d'avoir une unité de calcul séparée pour les adresses peut s'expliquer pour plusieurs raisons. Sur les processeurs normaux, la raison est que cela simplifie un peu l'implémentation des modes d'adressage indirects. Sur les rares processeurs qui ont des registres séparés pour les adresses, un banc de registre dédié est réservé aux registres d'adresses, ce qui rend l'usage d'une unité de calcul d'adresse bien plus pratique. Une autre raison se manifestait sur les processeurs 8 bits : ils géraient des données de 8 bits, mais des adresses de 16 bits. Dans ce cas, le processeur avait une ALU simple de 16 bits pour les adresses, et une ALU complexe de 8 bits pour les données.
[[File:Unité d'accès mémoire avec registres d'adresse ou d'indice.png|centre|vignette|upright=2|Unité d'accès mémoire avec registres d'adresse ou d'indice]]
===La gestion de l'alignement et du boutisme===
L'interface mémoire gère les accès mémoire non-alignés, à cheval sur deux mots mémoire (rappelez-vous le chapitre sur l'alignement mémoire). Elle détecte les accès mémoire non-alignés et réagit en conséquence. Dans le cas où les accès non-alignés sont interdits, elle lève une exception matérielle. Dans le cas où ils sont autorisés, elle les gère automatiquement, à savoir qu'elle charge deux mots mémoire et les combine entre eux pour donner le résultat final. Dans les deux cas, cela demande d'ajouter des circuits de détection des accès non-alignés, et éventuellement des circuits pour le double lecture/écriture.
Les circuits de détection des accès non-alignés sont très simples. Dans le cas où les adresses sont alignées sur une puissance de deux (cas le plus courant), il suffit de vérifier les bits de poids faible de l'adresse à lire. Prenons l'exemple d'un processeur avec des adresses codées sur 64 bits, avec des mots mémoire de 32 bits, alignés sur 32 bits (4 octets). Un mot mémoire contient 4 octets, les contraintes d'alignement font que les adresses autorisées sont des multiples de 4. En conséquence, les 2 bits de poids faible d'une adresse valide sont censés être à 0. En vérifiant la valeur de ces deux bits, on détecte facilement les accès non-alignés.
En clair, détecter les accès non-alignés demande de tester si les bits de poids faibles adéquats sont à 0. Il suffit donc d'un circuit de comparaison avec zéro; qui est une simple porte OU. Cette porte OU génère un bit qui indique si l'accès testé est aligné ou non : 1 si l'accès est non-aligné, 0 sinon. Le signal peut être transmis au séquenceur pour générer une exception matérielle, ou utilisé dans l'unité d'accès mémoire pour la double lecture/écriture.
La gestion automatique des accès non-alignés est plus complexe. Dans ce cas, l'unité mémoire charge deux mots mémoire et les combine entre eux pour donner le résultat final. Charger deux mots mémoires consécutifs est assez simple, si le registre d'interfaçage est un compteur. L'accès initial charge le premier mot mémoire, puis l'adresse stockée dans le registre d'interfaçage est incrémentée pour démarrer un second accès. Le circuit pour combiner deux mots mémoire contient des registres, des circuits de décalage, des multiplexeurs.
===Le rafraichissement mémoire optimisé et le contrôleur mémoire intégré===
Depuis les années 80, les processeurs sont souvent combinés avec une mémoire principale de type DRAM. De telles mémoires doivent être rafraichies régulièrement pour ne pas perdre de données. Le rafraichissement se fait généralement adresse par adresse, ou ligne par ligne (les lignes sont des super-bytes internes à la DRAM). Le rafraichissement est en théorie géré par le contrôleur mémoire installé sur la carte mère. Mais au tout début de l'informatique, du temps des processeurs 8 bits, le rafraichissement mémoire était géré directement par le processeur.
Si quelques processeurs géraient le rafraichissement mémoire avec des interruptions, d'autres processeurs disposaient d’optimisations pour optimiser le rafraichissement mémoire. Divers processeurs implémentaient de quoi faciliter le rafraichissement par adresse. Par exemple, le processeur Zilog Z80 contenait un compteur de ligne, un registre qui contenait le numéro de la prochaine ligne à rafraichir. Il était incrémenté à chaque rafraichissement mémoire, automatiquement, par le processeur lui-même. Un ''timer'' interne permettait de savoir quand rafraichir la mémoire : quand ce ''timer'' atteignait 0, une commande de rafraichissement était envoyée à la mémoire, et le ''timer'' était ''reset''. Et tout cela était intégré à l'unité d'accès mémoire.
Depuis les années 2000, les processeurs modernes ont un contrôleur mémoire DRAM intégré directement dans le processeur. Ce qui fait qu'ils gèrent non seulement le rafraichissement, mais aussi d'autres fonctions bien pus complexes.
==Le chemin de données et son réseau d'interconnexions==
Nous venons de voir que le chemin de données contient une unité de calcul (parfois plusieurs), des registres isolés, un banc de registre, une unité mémoire. Le tout est chapeauté par une unité de contrôle qui commande le chemin de données, qui fera l'objet des prochains chapitres. Mais il faut maintenant relier registres, ALU et unité mémoire pour que l'ensemble fonctionne. Pour cela, diverses interconnexions internes au processeur se chargent de relier le tout.
Sur les anciens processeurs, les interconnexions sont assez simples et se résument à un ou deux '''bus internes au processeur''', reliés au bus mémoire. C'était la norme sur des architectures assez ancienne, qu'on n'a pas encore vu à ce point du cours, appelées les architectures à accumulateur et à pile. Mais ce n'est plus la solution utilisée actuellement. De nos jours, le réseaux d'interconnexion intra-processeur est un ensemble de connexions point à point entre ALU/registres/unité mémoire. Et paradoxalement, cela rend plus facile de comprendre ce réseau d'interconnexion.
===Introduction propédeutique : l'implémentation des modes d'adressage principaux===
L'organisation interne du processeur dépend fortement des modes d'adressage supportés. Pour simplifier les explications, nous allons séparer les modes d'adressage qui gèrent les pointeurs et les autres. Suivant que le processeur supporte les pointeurs ou non, l'organisation des bus interne est légèrement différente. La différence se voit sur les connexions avec le bus d'adresse et de données.
Tout processeur gère au minimum le '''mode d'adressage absolu''', où l'adresse est intégrée à l'instruction. Le séquenceur extrait l'adresse mémoire de l'instruction, et l'envoie sur le bus d'adresse. Pour cela, le séquenceur est relié au bus d'adresse, le chemin de donnée est relié au bus de données. Le chemin de donnée n'est pas connecté au bus d'adresse, il n'y a pas d'autres connexions.
[[File:Chemin de données sans support des pointeurs.png|centre|vignette|upright=2|Chemin de données sans support des pointeurs]]
Le '''support des pointeurs''' demande d'intégrer des modes d'adressage dédiés : l'adressage indirect à registre, l'adresse base + indice, et les autres. Les pointeurs sont stockés dans le banc de registre et sont modifiés par l'unité de calcul. Pour supporter les pointeurs, le chemin de données est connecté sur le bus d'adresse avec le séquenceur. Suivant le mode d'adressage, le bus d'adresse est relié soit au chemin de données, soit au séquenceur.
[[File:Chemin de données avec support des pointeurs.png|centre|vignette|upright=2|Chemin de données avec support des pointeurs]]
Pour terminer, il faut parler des instructions de '''copie mémoire vers mémoire''', qui copient une donnée d'une adresse mémoire vers une autre. Elles ne se passent pas vraiment dans le chemin de données, mais se passent purement au niveau des registres d’interfaçage. L'usage d'un registre d’interfaçage unique permet d'implémenter ces instructions très facilement. Elle se fait en deux étapes : on copie la donnée dans le registre d’interfaçage, on l'écrit en mémoire RAM. L'adresse envoyée sur le bus d'adresse n'est pas la même lors des deux étapes.
===Le banc de registre est multi-port, pour gérer nativement les opérations dyadiques===
Les architectures RISC et CISC incorporent un banc de registre, qui est connecté aux unités de calcul et au bus mémoire. Et ce banc de registre peut être mono-port ou multiport. S'il a existé d'anciennes architectures utilisant un banc de registre mono-port, elles sont actuellement obsolètes. Nous les aborderons dans un chapitre dédié aux architectures dites canoniques, mais nous pouvons les laisser de côté pour le moment. De nos jours, tous les processeurs utilisent un banc de registre multi-port.
[[File:Chemin de données minimal d'une architecture LOAD-STORE (sans MOV inter-registres).png|centre|vignette|upright=2|Chemin de données minimal d'une architecture LOAD-STORE (sans MOV inter-registres)]]
Le banc de registre multiport est optimisé pour les opérations dyadiques. Il dispose précisément de deux ports de lecture et d'un port d'écriture pour l'écriture. Un port de lecture par opérande et le port d'écriture pour enregistrer le résultat. En clair, le processeur peut lire deux opérandes et écrire un résultat en un seul cycle d'horloge. L'avantage est que les opérations simples ne nécessitent qu'une micro-opération, pas plus.
[[File:ALU data paths.svg|centre|vignette|upright=1.5|Processeur LOAD-STORE avec un banc de registre multiport, avec les trois ports mis en évidence.]]
===Une architecture LOAD-STORE basique, avec adressage absolu===
Voyons maintenant comment l'implémentation d'une architecture RISC très simple, qui ne supporte pas les adressages pour les pointeurs, juste les adressages inhérent (à registres) et absolu (par adresse mémoire). Les instructions LOAD et STORE utilisent l'adressage absolu, géré par le séquenceur, reste à gérer l'échange entre banc de registres et bus de données. Une lecture LOAD relie le bus de données au port d'écriture du banc de registres, alors que l'écriture relie le bus au port de lecture du banc de registre. Pour cela, il faut ajouter des multiplexeurs sur les chemins existants, comme illustré par le schéma ci-dessous.
[[File:Bus interne au processeur sur archi LOAD STORE avec banc de registres multiport.png|centre|vignette|upright=2|Organisation interne d'une architecture LOAD STORE avec banc de registres multiport. Nous n'avons pas représenté les signaux de commandes envoyés par le séquenceur au chemin de données.]]
Ajoutons ensuite les instructions de copie entre registres, souvent appelées instruction COPY ou MOV. Elles existent sur la plupart des architectures LOAD-STORE. Une première solution boucle l'entrée du banc de registres sur son entrée, ce qui ne sert que pour les copies de registres.
[[File:Chemin de données d'une architecture LOAD-STORE.png|centre|vignette|upright=2|Chemin de données d'une architecture LOAD-STORE]]
Mais il existe une seconde solution, qui ne demande pas de modifier le chemin de données. Il est possible de faire passer les copies de données entre registres par l'ALU. Lors de ces copies, l'ALU une opération ''Pass through'', à savoir qu'elle recopie une des opérandes sur sa sortie. Le fait qu'une ALU puisse effectuer une opération ''Pass through'' permet de fortement simplifier le chemin de donnée, dans le sens où cela permet d'économiser des multiplexeurs. Mais nous verrons cela sous peu. D'ailleurs, dans la suite du chapitre, nous allons partir du principe que les copies entre registres passent par l'ALU, afin de simplifier les schémas.
===L'ajout des modes d'adressage indirects à registre pour les pointeurs===
Passons maintenant à l'implémentation des modes d'adressages pour les pointeurs. Avec eux, l'adresse mémoire à lire/écrire n'est pas intégrée dans une instruction, mais est soit dans un registre, soit calculée par l'ALU.
Le premier mode d'adressage de ce type est le mode d'adressage indirect à registre, où l'adresse à lire/écrire est dans un registre. L'implémenter demande donc de connecter la sortie du banc de registres au bus d'adresse. Il suffit d'ajouter un MUX en sortie d'un port de lecture.
[[File:Chemin de données à trois bus.png|centre|vignette|upright=2|Chemin de données à trois bus.]]
Le mode d'adressage base + indice est un mode d'adressage où l'adresse à lire/écrire est calculée à partir d'une adresse et d'un indice, tous deux présents dans un registre. Le calcul de l'adresse implique au minimum une addition et donc l'ALU. Dans ce cas, on doit connecter la sortie de l'unité de calcul au bus d'adresse.
[[File:Bus avec adressage base+index.png|centre|vignette|upright=2|Bus avec adressage base+index]]
Le chemin de données précédent gère aussi le mode d'adressage indirect avec pré-décrément. Pour rappel, ce mode d'adressage est une variante du mode d'adressage indirect, qui utilise une pointeur/adresse stocké dans un registre. La différence est que ce pointeur est décrémenté avant d'être envoyé sur le bus d'adresse. L'implémentation matérielle est la même que pour le mode Base + Indice : l'adresse est lue depuis les registres, décrémentée dans l'ALU, et envoyée sur le bus d'adresse.
Le schéma précédent montre que le bus d'adresse est connecté à un MUX avant l'ALU et un autre MUX après. Mais il est possible de se passer du premier MUX, utilisé pour le mode d'adressage indirect à registre. La condition est que l'ALU supporte l'opération ''pass through'', un NOP, qui recopie une opérande sur sa sortie. L'ALU fera une opération NOP pour le mode d'adressage indirect à registre, un calcul d'adresse pour le mode d'adressage base + indice. Par contre, faire ainsi rendra l'adressage indirect légèrement plus lent, vu que le temps de passage dans l'ALU sera compté.
[[File:Bus avec adressage indirect.png|centre|vignette|upright=2|Bus avec adressages pour les pointeurs, simplifié.]]
Dans ce qui va suivre, nous allons partir du principe que le processeur est implémenté en suivant le schéma précédent, afin d'avoir des schéma plus lisibles.
===L'adressage immédiat et les modes d'adressages exotiques===
Passons maintenant au mode d’adressage immédiat, qui permet de préciser une constante dans une instruction directement. La constante est extraite de l'instruction par le séquenceur, puis insérée au bon endroit dans le chemin de données. Pour les opérations arithmétiques/logiques/branchements, il faut insérer la constante extraite sur l'entrée de l'ALU. Sur certains processeurs, la constante peut être négative et doit alors subir une extension de signe dans un circuit spécialisé.
[[File:Chemin de données - Adressage immédiat avec extension de signe.png|centre|vignette|upright=2|Chemin de données - Adressage immédiat avec extension de signe.]]
L'implémentation précédente gère aussi les modes d'adressage base + décalage et absolu indexé. Pour rappel, le premier ajoute une constante à une adresse prise dans les registres, le second prend une adresse constante et lui ajoute un indice pris dans les registres. Dans les deux cas, on lit un registre, extrait une constante/adresse de l’instruction, additionne les deux dans l'ALU, avant d'envoyer le résultat sur le bus d'adresse. La seule difficulté est de désactiver l'extension de signe pour les adresses.
Le mode d'adressage absolu peut être traité de la même manière, si l'ALU est capable de faire des NOPs. L'adresse est insérée au même endroit que pour le mode d'adressage immédiat, parcours l'unité de calcul inchangée parce que NOP, et termine sur le bus d'adresse.
[[File:Chemin de données avec une ALU capable de faire des NOP.png|centre|vignette|upright=2|Chemin de données avec adressage immédiat étendu pour gérer des adresses.]]
Passons maintenant au cas particulier d'une instruction MOV qui copie une constante dans un registre. Il n'y a rien à faire si l'unité de calcul est capable d'effectuer une opération NOP/''pass through''. Pour charger une constante dans un registre, l'ALU est configurée pour faire un NOP, la constante traverse l'ALU et se retrouve dans les registres. Si l'ALU ne gère pas les NOP, la constante doit être envoyée sur l'entrée d'écriture du banc de registres, à travers un MUX dédié.
[[File:Implémentation de l'adressage immédiat dans le chemin de données.png|centre|vignette|upright=2|Implémentation de l'adressage immédiat dans le chemin de données]]
===Les architectures CISC : les opérations ''load-op''===
Tout ce qu'on a vu précédemment porte sur les processeurs de type LOAD-STORE, souvent confondus avec les processeurs de type RISC, où les accès mémoire sont séparés des instructions utilisant l'ALU. Il est maintenant temps de voir les processeurs CISC, qui gèrent des instructions ''load-op'', qui peuvent lire une opérande depuis la mémoire.
L'implémentation des opérations ''load-op'' relie le bus de donnée directement sur une entrée de l'unité de calcul, en utilisant encore une fois un multiplexeur. L'implémentation parait simple, mais c'est parce que toute la complexité est déportée dans le séquenceur. C'est lui qui se charge de détecter quand la lecture de l'opérande est terminée, quand l'opérande est disponible.
Les instructions ''load-op'' s'exécutent en plusieurs étapes, en plusieurs micro-opérations. Il y a typiquement une étape pour l'opérande à lire en mémoire et une étape de calcul. L'usage d'un registre d’interfaçage permet d'implémenter les instructions ''load-op'' très facilement. Une opération ''load-op'' charge l'opérande en mémoire dans un registre d’interfaçage, puis relier ce registre d’interfaçage sur une des entrées de l'ALU. Un simple multiplexeur suffit pour implémenter le tout, en plus des modifications adéquates du séquenceur.
[[File:Chemin de données d'un CPU CISC avec lecture des opérandes en mémoire.png|centre|vignette|upright=2|Chemin de données d'un CPU CISC avec lecture des opérandes en mémoire]]
Supporter les instructions multi-accès (qui font plusieurs accès mémoire) ne modifie pas fondamentalement le réseau d'interconnexion, ni le chemin de données La raison est que supporter les instructions multi-accès se fait au niveau du séquenceur. En réalité, les accès mémoire se font en série, l'un après l'autre, sous la commande du séquenceur qui émet plusieurs micro-opérations mémoire consécutives. Les données lues sont placées dans des registres d’interactivement mémoire, ce qui demande d'ajouter des registres d’interfaçage mémoire en plus.
==Annexe : le cas particulier du pointeur de pile==
Le pointeur de pile est un registre un peu particulier. Il peut être placé dans le chemin de données ou dans le séquenceur, voire dans l'unité de chargement, tout dépend du processeur. Tout dépend de si le pointeur de pile gère une pile d'adresses de retour ou une pile d'appel.
===Le pointeur de pile non-adressable explicitement===
Avec une pile d'adresse de retour, le pointeur de pile n'est pas adressable explicitement, il est juste adressé implicitement par des instructions d'appel de fonction CALL et des instructions de retour de fonction RET. Le pointeur de pile est alors juste incrémenté ou décrémenté par un pas constant, il ne subit pas d'autres opérations, son adressage est implicite. Il est juste incrémenté/décrémenté par pas constants, qui sont fournis par le séquenceur. Il n'y a pas besoin de le relier au chemin de données, vu qu'il n'échange pas de données avec les autres registres. Il y a alors plusieurs solutions, mais la plus simple est de placer le pointeur de pile dans le séquenceur et de l'incrémenter par un incrémenteur dédié.
Quelques processeurs simples disposent d'une pile d'appel très limitée, où le pointeur de pile n'est pas adressable explicitement. Il est adressé implicitement par les instruction CALL, RET, mais aussi PUSH et POP, mais aucune autre instruction ne permet cela. Là encore, le pointeur de pile ne communique pas avec les autres registres. Il est juste incrémenté/décrémenté par pas constants, qui sont fournis par le séquenceur. Là encore, le plus simple est de placer le pointeur de pile dans le séquenceur et de l'incrémenter par un incrémenteur dédié.
Dans les deux cas, le pointeur de pile est placé dans l'unité de contrôle, le séquenceur, et est associé à un incrémenteur dédié. Il se trouve que cet incrémenteur est souvent partagé avec le ''program counter''. En effet, les deux sont des adresses mémoire, qui sont incrémentées et décrémentées par pas constants, ne subissent pas d'autres opérations (si ce n'est des branchements, mais passons). Les ressemblances sont suffisantes pour fusionner les deux circuits. Ils peuvent donc avoir un '''incrémenteur partagé'''.
L'incrémenteur en question est donc partagé entre pointeur de pile, ''program counter'' et quelques autres registres similaires. Par exemple, le Z80 intégrait un registre pour le rafraichissement mémoire, qui était réalisé par le CPU à l'époque. Ce registre contenait la prochaine adresse mémoire à rafraichir, et était incrémenté à chaque rafraichissement d'une adresse. Et il était lui aussi intégré au séquenceur et incrémenté par l'incrémenteur partagé.
[[File:Organisation interne d'une architecture à pile.png|centre|vignette|upright=2|Organisation interne d'une architecture à pile]]
===Le pointeur de pile adressable explicitement===
Maintenant, étudions le cas d'une pile d'appel, précisément d'une pile d'appel avec des cadres de pile de taille variable. Sous ces conditions, le pointeur de pile est un registre adressable, avec un nom/numéro de registre dédié. Tel est par exemple le cas des processeurs x86 avec le registre ESP (''Extended Stack Pointer''). 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 calculs d'adresse, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres.
Dans ce cas, la meilleure solution est de placer le pointeur de pile dans le banc de registre généraux, avec les autres registres entiers. En faisant cela, la manipulation du pointeur de pile est faite par l'unité de calcul entière, pas besoin d'utiliser un incrémenteur dédiée. Il a existé des processeurs qui mettaient le pointeur de pile dans le banc de registre, mais l'incrémentaient avec un incrémenteur dédié, mais nous les verrons dans le chapitre sur les architectures à accumulateur. La raison est que sur les processeurs concernés, les adresses ne faisaient pas la même taille que les données : c'était des processeurs 8 bits, qui géraient des adresses de 16 bits.
==Annexe : l'implémentation du système d'''aliasing'' des registres des CPU x86==
Il y a quelques chapitres, nous avions parlé du système d'''aliasing'' des registres des CPU x86. Pour rappel, il permet de donner plusieurs noms de registre pour un même registre. Plus précisément, pour un registre 64 bits, le registre complet aura un nom de registre, les 32 bits de poids faible auront leur nom de registre dédié, idem pour les 16 bits de poids faible, etc. Il est possible de faire des calculs sur ces moitiés/quarts/huitièmes de registres sans problème.
===L'''aliasing'' du 8086, pour les registres 16 bits===
[[File:Register 8086.PNG|vignette|Register 8086]]
L'implémentation de l'''aliasing'' est apparue sur les premiers CPU Intel 16 bits, notamment le 8086. En tout, ils avaient quatre registres généraux 16 bits : AX, BX, CX et DX. Ces quatre registres 16 bits étaient coupés en deux octets, chacun adressable. Par exemple, le registre AX était coupé en deux octets nommés AH et AL, chacun ayant son propre nom/numéro de registre. Les instructions d'addition/soustraction pouvaient manipuler le registre AL, ou le registre AH, ce qui modifiait les 8 bits de poids faible ou fort selon le registre choisit.
Le banc de registre ne gére que 4 registres de 16 bits, à savoir AX, BX, CX et DX. Lors d'une lecture d'un registre 8 bits, le registre 16 bit entier est lu depuis le banc de registre, mais les bits inutiles sont ignorés. Par contre, l'écriture peut se faire soit avec 16 bits d'un coup, soit pour seulement un octet. Le port d'écriture du banc de registre peut être configuré de manière à autoriser l'écriture soit sur les 16 bits du registre, soit seulement sur les 8 bits de poids faible, soit écrire dans les 8 bits de poids fort.
[[File:Port d'écriture du banc de registre du 8086.png|centre|vignette|upright=2.5|Port d'écriture du banc de registre du 8086]]
Une opération sur un registre 8 bits se passe comme suit. Premièrement, on lit le registre 16 bits complet depuis le banc de registre. Si l'on a sélectionné l'octet de poids faible, il ne se passe rien de particulier, l'opérande 16 bits est envoyée directement à l'ALU. Mais si on a sélectionné l'octet de poids fort, la valeur lue est décalée de 7 rangs pour atterrir dans les 8 octets de poids faible. Ensuite, l'unité de calcul fait un calcul avec cet opérande, un calcul 16 bits tout ce qu'il y a de plus classique. Troisièmement, le résultat est enregistré dans le banc de registre, en le configurant convenablement. La configuration précise s'il faut enregistrer le résultat dans un registre 16 bits, soit seulement dans l'octet de poids faible/fort.
Afin de simplifier le câblage, les 16 bits des registres AX/BX/CX/DX sont entrelacés d'une manière un peu particulière. Intuitivement, on s'attend à ce que les bits soient physiquement dans le même ordre que dans le registre : le bit 0 est placé à côté du bit 1, suivi par le bit 2, etc. Mais à la place, l'octet de poids fort et de poids faible sont mélangés. Deux bits consécutifs appartiennent à deux octets différents. Le tout est décrit dans le tableau ci-dessous.
{|class="wikitable"
|-
! Registre 16 bits normal
| class="f_bleu" | 15
| class="f_bleu" | 14
| class="f_bleu" | 13
| class="f_bleu" | 12
| class="f_bleu" | 11
| class="f_bleu" | 10
| class="f_bleu" | 9
| class="f_bleu" | 8
| class="f_rouge" | 7
| class="f_rouge" | 6
| class="f_rouge" | 5
| class="f_rouge" | 4
| class="f_rouge" | 3
| class="f_rouge" | 2
| class="f_rouge" | 1
| class="f_rouge" | 0
|-
! Registre 16 bits du 8086
| class="f_bleu" | 15
| class="f_rouge" | 7
| class="f_bleu" | 14
| class="f_rouge" | 6
| class="f_bleu" | 13
| class="f_rouge" | 5
| class="f_bleu" | 12
| class="f_rouge" | 4
| class="f_bleu" | 11
| class="f_rouge" | 3
| class="f_bleu" | 10
| class="f_rouge" | 2
| class="f_bleu" | 9
| class="f_rouge" | 1
| class="f_bleu" | 8
| class="f_rouge" | 0
|}
En faisant cela, le décaleur en entrée de l'ALU est bien plus simple. Il y a 8 multiplexeurs, mais le câblage est bien plus simple. Par contre, en sortie de l'ALU, il faut remettre les bits du résultat dans l'ordre adéquat, celui du registre 8086. Pour cela, les interconnexions sur le port d'écriture sont conçues pour. Il faut juste mettre les fils de sortie de l'ALU sur la bonne entrée, par besoin de multiplexeurs.
===L'''aliasing'' sur les processeurs x86 32/64 bits===
Les processeurs x86 32 et 64 bits ont un système d'''aliasing'' qui complète le système précédent. Les processeurs 32 bits étendent les registres 16 bits existants à 32 bits. Pour ce faire, le registre 32 bit a un nouveau nom de registre, distincts du nom de registre utilisé pour l'ancien registre 16 bits. Il est possible d'adresser les 16 bits de poids faible de ce registre, avec le même nom de registre que celui utilisé pour le registre 16 sur les processeurs d'avant. Même chose avec les processeurs 64, avec l'ajout d'un nouveau nom de registre pour adresser un registre de 64 bit complet.
En soit, implémenter ce système n'est pas compliqué. Prenons le cas du registre RAX (64 bits), et de ses subdivisions nommées EAX (32 bits), AX (16 bits). À l'intérieur du banc de registre, il n'y a que le registre RAX. Le banc de registre ne comprend qu'un seul nom de registre : RAX. Les subdivisions EAX et AX n'existent qu'au niveau de l'écriture dans le banc de registre. L'écriture dans le banc de registre est configurable, de manière à ne modifier que les bits adéquats. Le résultat d'un calcul de l'ALU fait 64 bits, il est envoyé sur le port d'écriture. À ce niveau, soit les 64 bits sont écrits dans le registre, soit seulement les 32/16 bits de poids faible. Le système du 8086 est préservé pour les écritures dans les 16 bits de poids faible.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les composants d'un processeur
| prevText=Les composants d'un processeur
| next=L'unité de chargement et le program counter
| nextText=L'unité de chargement et le program counter
}}
</noinclude>
41xe9wt14nwmn1f97j94epkpb6w4geh
745837
745836
2025-07-02T20:43:44Z
Mewtow
31375
/* Les circuits multiplieurs et diviseurs */
745837
wikitext
text/x-wiki
Comme vu précédemment, le '''chemin de donnée''' est l'ensemble des composants dans lesquels circulent les données dans le processeur. Il comprend l'unité de calcul, les registres, l'unité de communication avec la mémoire, et le ou les interconnexions qui permettent à tout ce petit monde de communiquer. Dans ce chapitre, nous allons voir ces composants en détail.
==Les unités de calcul==
Le processeur contient des circuits capables de faire des calculs arithmétiques, des opérations logiques, et des comparaisons, qui sont regroupés dans une unité de calcul appelée '''unité arithmétique et logique'''. Certains préfèrent l’appellation anglaise ''arithmetic and logic unit'', ou ALU. Par défaut, ce terme est réservé aux unités de calcul qui manipulent des nombres entiers. Les unités de calcul spécialisées pour les calculs flottants sont désignées par le terme "unité de calcul flottant", ou encore FPU (''Floating Point Unit'').
L'interface d'une unité de calcul est assez simple : on a des entrées pour les opérandes et une sortie pour le résultat du calcul. De plus, les instructions de comparaisons ou de calcul peuvent mettre à jour le registre d'état, qui est relié à une autre sortie de l’unité de calcul. Une autre entrée, l''''entrée de sélection de l'instruction''', spécifie l'opération à effectuer. Elle sert à configurer l'unité de calcul pour faire une addition et pas une multiplication, par exemple. Sur cette entrée, on envoie un numéro qui précise l'opération à effectuer. La correspondance entre ce numéro et l'opération à exécuter dépend de l'unité de calcul. Sur les processeurs où l'encodage des instructions est "simple", une partie de l'opcode de l'instruction est envoyé sur cette entrée.
[[File:Unité de calcul usuelle.png|centre|vignette|upright=2|Unité de calcul usuelle.]]
Il faut signaler que les processeurs modernes possèdent plusieurs unités de calcul, toutes reliées aux registres. Cela permet d’exécuter plusieurs calculs en même temps dans des unités de calcul différentes, afin d'augmenter les performances du processeur. Diverses technologies, abordées dans la suite du cours permettent de profiter au mieux de ces unités de calcul : pipeline, exécution dans le désordre, exécution superscalaire, jeux d'instructions VLIW, etc. Mais laissons cela de côté pour le moment.
===L'ALU entière : additions, soustractions, opérations bit à bit===
Un processeur contient plusieurs ALUs spécialisées. La principale, présente sur tous les processeurs, est l''''ALU entière'''. Elle s'occupe uniquement des opérations sur des nombres entiers, les nombres flottants sont gérés par une ALU à part. Elle gère des opérations simples : additions, soustractions, opérations bit à bit, parfois des décalages/rotations. Par contre, elle ne gère pas la multiplication et la division, qui sont prises en charge par un circuit multiplieur/diviseur à part.
L'ALU entière a déjà été vue dans un chapitre antérieur, nommé "Les unités arithmétiques et logiques entières (simples)", qui expliquait comment en concevoir une. Nous avions vu qu'une ALU entière est une sorte de circuit additionneur-soustracteur amélioré, ce qui explique qu'elle gère des opérations entières simples, mais pas la multiplication ni la division. Nous ne reviendrons pas dessus. Cependant, il y a des choses à dire sur leur intégration au processeur.
Une ALU entière gère souvent une opération particulière, qui ne fait rien et recopie simplement une de ses opérandes sur sa sortie. L'opération en question est appelée l''''opération ''Pass through''''', encore appelée opération NOP. Elle est implémentée en utilisant un simple multiplexeur, placé en sortie de l'ALU. Le fait qu'une ALU puisse effectuer une opération ''Pass through'' permet de fortement simplifier le chemin de donnée, d'économiser des multiplexeurs. Mais nous verrons cela sous peu.
[[File:ALU avec opération NOP.png|centre|vignette|upright=2|ALU avec opération NOP.]]
Avant l'invention du microprocesseur, le processeur n'était pas un circuit intégré unique. L'ALU, le séquenceur et les registres étaient dans des puces séparées. Les ALU étaient vendues séparément et manipulaient des opérandes de 4/8 bits, les ALU 4 bits étaient très fréquentes. Si on voulait créer une ALU pour des opérandes plus grandes, il fallait construire l'ALU en combinant plusieurs ALU 4/8 bits. Par exemple, l'ALU des processeurs AMD Am2900 est une ALU de 16 bits composée de plusieurs sous-ALU de 4 bits. Cette technique qui consiste à créer des unités de calcul à partir d'unités de calcul plus élémentaires s'appelle en jargon technique du '''bit slicing'''. Nous en avions parlé dans le chapitre sur les unités de calcul, aussi nous n'en reparlerons pas plus ici.
L'ALU manipule des opérandes codées sur un certain nombre de bits. Par exemple, une ALU peut manipuler des entiers codés sur 8 bits, sur 16 bits, etc. En général, la taille des opérandes de l'ALU est la même que la taille des registres. Un processeur 32 bits, avec des registres de 32 bit, a une ALU de 32 bits. C'est intuitif, et cela rend l'implémentation du processeur bien plus facile. Mais il y a quelques exceptions, où l'ALU manipule des opérandes plus petits que la taille des registres. Par exemple, de nombreux processeurs 16 bits, avec des registres de 16 bits, utilisent une ALU de 8 bits. Un autre exemple assez connu est celui du Motorola 68000, qui était un processeur 32 bits, mais dont l'ALU faisait juste 16 bits. Son successeur, le 68020, avait lui une ALU de 32 bits.
Sur de tels processeurs, les calculs sont fait en plusieurs passes. Par exemple, avec une ALU 8 bit, les opérations sur des opérandes 8 bits se font en un cycle d'horloge, celles sur 16 bits se font en deux cycles, celles en 32 en quatre, etc. Si un programme manipule assez peu d'opérandes 16/32/64 bits, la perte de performance est assez faible. Diverses techniques visent à améliorer les performances, mais elles ne font pas de miracles. Par exemple, vu que l'ALU est plus courte, il est possible de la faire fonctionner à plus haute fréquence, pour réduire la perte de performance.
Pour comprendre comme est implémenté ce système de passes, prenons l'exemple du processeur 8 bit Z80. Ses registres entiers étaient des registres de 8 bits, alors que l'ALU était de 4 bits. Les calculs étaient faits en deux phases : une qui traite les 4 bits de poids faible, une autre qui traite les 4 bits de poids fort. Pour cela, les opérandes étaient placées dans des registres de 4 bits en entrée de l'ALU, plusieurs multiplexeurs sélectionnaient les 4 bits adéquats, le résultat était mémorisé dans un registre de résultat de 8 bits, un démultiplexeur plaçait les 4 bits du résultat au bon endroit dans ce registre. L'unité de contrôle s'occupait de la commande des multiplexeurs/démultiplexeurs. Les autres processeurs 8 ou 16 bits utilisent des circuits similaires pour faire leurs calculs en plusieurs fois.
[[File:ALU du Z80.png|centre|vignette|upright=2|ALU du Z80]]
Un exemple extrême est celui des des '''processeurs sériels''' (sous-entendu ''bit-sériels''), qui utilisent une '''ALU sérielle''', qui fait leurs calculs bit par bit, un bit à la fois. S'il a existé des processeurs de 1 bit, comme le Motorola MC14500B, la majeure partie des processeurs sériels étaient des processeurs 4, 8 ou 16 bits. L'avantage de ces ALU est qu'elles utilisent peu de transistors, au détriment des performances par rapport aux processeurs non-sériels. Mais un autre avantage est qu'elles peuvent gérer des opérandes de grande taille, avec plus d'une trentaine de bits, sans trop de problèmes.
===Les circuits multiplieurs et diviseurs===
Les processeurs modernes ont une ALU pour les opérations simples (additions, décalages, opérations logiques), couplée à une ALU pour les multiplications, un circuit multiplieur séparé. Précisons qu'il ne sert pas à grand chose de fusionner le circuit multiplieur avec l'ALU, mieux vaut les garder séparés par simplicité. Les processeurs haute performance disposent systématiquement d'un circuit multiplieur et gèrent la multiplication dans leur jeu d'instruction.
Le cas de la division est plus compliqué. La présence d'un circuit multiplieur est commune, mais les circuits diviseurs sont eux très rares. Leur cout en circuit est globalement le même que pour un circuit multiplieur, mais le gain en performance est plus faible. Le gain en performance pour la multiplication est modéré car il s'agit d'une opération très fréquente, alors qu'il est très faible pour la division car celle-ci est beaucoup moins fréquente.
Pour réduire le cout en circuits, il arrive que l'ALU pour les multiplications gère à la fois la multiplication et la division. Les circuits multiplieurs et diviseurs sont en effet très similaires et partagent beaucoup de points communs. Généralement, la fusion se fait pour les multiplieurs/diviseurs itératifs.
===Le ''barrel shifter''===
On vient d'expliquer que la présence de plusieurs ALU spécialisée est très utile pour implémenter des opérations compliquées à insérer dans une unité de calcul normale, comme la multiplication et la division. Mais les décalages sont aussi dans ce cas, de même que les rotations. Nous avions vu il y a quelques chapitres qu'ils sont réalisés par un circuit spécialisé, appelé un ''barrel shifter'', qu'il est difficile de fusionner avec une ALU normale. Aussi, beaucoup de processeurs incorporent un ''barrel shifter'' séparé de l'ALU.
Les processeurs ARM utilise un ''barrel shifter'', mais d'une manière un peu spéciale. On a vu il y a quelques chapitres que si on fait une opération logique, une addition, une soustraction ou une comparaison, la seconde opérande peut être décalée automatiquement. L'instruction incorpore le type de de décalage à faire et par combien de rangs il faut décaler directement à côté de l'opcode. Cela simplifie grandement les calculs d'adresse, qui se font en une seule instruction, contre deux ou trois sur d'autres architectures. Et pour cela, l'ALU proprement dite est précédée par un ''barrel shifter'',une seconde ALU spécialisée dans les décalages. Notons que les instructions MOV font aussi partie des instructions où la seconde opérande (le registre source) peut être décalé : cela signifie que les MOV passent par l'ALU, qui effectue alors un NOP, une opération logique OUI.
===Les unités de calcul spécialisées===
Un processeur peut disposer d’unités de calcul séparées de l'unité de calcul principale, spécialisées dans les décalages, les divisions, etc. Et certaines d'entre elles sont spécialisées dans des opérations spécifiques, qui ne sont techniquement pas des opérations entières, sur des nombres entiers.
[[File:Unité de calcul flottante, intérieur.png|vignette|upright=1|Unité de calcul flottante, intérieur]]
Depuis les années 90-2000, presque tous les processeurs utilisent une unité de calcul spécialisée pour les nombres flottants : la '''Floating-Point Unit''', aussi appelée FPU. En général, elle regroupe un additionneur-soustracteur flottant et un multiplieur flottant. Parfois, elle incorpore un diviseur flottant, tout dépend du processeur. Précisons que sur certains processeurs, la FPU et l'ALU entière ne vont pas à la même fréquence, pour des raisons de performance et de consommation d'énergie !
La FPU intègre un circuit multiplieur entier, utilisé pour les multiplications flottantes, afin de multiplier les mantisses entre elles. Quelques processeurs utilisaient ce multiplieur pour faire les multiplications entières. En clair, au lieu d'avoir un multiplieur entier séparé du multiplieur flottant, les deux sont fusionnés en un seul circuit. Il s'agit d'une optimisation qui a été utilisée sur quelques processeurs 32 bits, qui supportaient les flottants 64 bits (double précision). Les processeurs Atom étaient dans ce cas, idem pour l'Athlon première génération. Les processeurs modernes n'utilisent pas cette optimisation pour des raisons qu'on ne peut pas expliquer ici (réduction des dépendances structurelles, émission multiple).
Il existe des unités de calcul spécialisées pour les calculs d'adresse. Elles ne supportent guère plus que des incrémentations/décrémentations, des additions/soustractions, et des décalages simples. Les autres opérations n'ont pas de sens avec des adresses. L'usage d'ALU spécialisées pour les adresses est un avantage sur les processeurs où les adresses ont une taille différente des données, ce qui est fréquent sur les anciens processeurs 8 bits.
De nombreux processeurs modernes disposent d'une unité de calcul spécialisée dans le calcul des conditions, tests et branchements. C’est notamment le cas sur les processeurs sans registre d'état, qui disposent de registres à prédicats. En général, les registres à prédicats sont placés à part des autres registres, dans un banc de registre séparé. L'unité de calcul normale n'est pas reliée aux registres à prédicats, alors que l'unité de calcul pour les branchements/test/conditions l'est. Les registres à prédicats sont situés juste en sortie de cette unité de calcul.
==Les registres du processeur==
Après avoir vu l'unité de calcul, il est temps de passer aux registres d'un processeur. L'organisation des registres est généralement assez compliquée, avec quelques registres séparés des autres comme le registre d'état ou le ''program counter''. Les registres d'un processeur peuvent se classer en deux camps : soit ce sont des registres isolés, soit ils sont regroupés en paquets appelés banc de registres.
Un '''banc de registres''' (''register file'') est une RAM, dont chaque byte est un registre. Il regroupe un paquet de registres différents dans un seul composant, dans une seule mémoire. Dans processeur moderne, on trouve un ou plusieurs bancs de registres. La répartition des registres, à savoir quels registres sont dans le banc de registre et quels sont ceux isolés, est très variable suivant les processeurs.
[[File:Register File Simple.svg|centre|vignette|upright=1|Banc de registres simplifié.]]
===L'adressage du banc de registres===
Le banc de registre est une mémoire comme une autre, avec une entrée d'adresse qui permet de sélectionner le registre voulu. Plutot que d'adresse, nous allons parler d''''identifiant de registre'''. Le séquenceur forge l'identifiant de registre en fonction des registres sélectionnés. Dans les chapitres précédents, nous avions vu qu'il existe plusieurs méthodes pour sélectionner un registre, qui portent les noms de modes d'adressage. Et bien les modes d'adressage jouent un grand rôle dans la forge de l'identifiant de registre.
Pour rappel, sur la quasi-totalité des processeurs actuels, les registres généraux sont identifiés par un nom de registre, terme trompeur vu que ce nom est en réalité un numéro. En clair, les processeurs numérotent les registres, le numéro/nom du registre permettant de l'identifier. Par exemple, si je veux faire une addition, je dois préciser les deux registres pour les opérandes, et éventuellement le registre pour le résultat : et bien ces registres seront identifiés par un numéro. Mais tous les registres ne sont pas numérotés et ceux qui ne le sont pas sont adressés implicitement. Par exemple, le pointeur de pile sera modifié par les instructions qui manipulent la pile, sans que cela aie besoin d'être précisé par un nom de registre dans l'instruction.
Dans le cas le plus simple, les registres nommés vont dans le banc de registres, les registres adressés implicitement sont en-dehors, dans des registres isolés. L'idéntifiant de registre est alors simplement le nom de registre, le numéro. Le séquenceur extrait ce nom de registre de l'insutrction, avant de l'envoyer sur l'entrée d'adresse du banc de registre.
[[File:Adressage du banc de registres généruax.png|centre|vignette|upright=2|Adressage du banc de registres généraux]]
Dans un cas plus complexe, des registres non-nommés sont placés dans le banc de registres. Par exemple, les pointeurs de pile sont souvent placés dans le banc de registre, même s'ils sont adressés implicitement. Même des registres aussi importants que le ''program counter'' peuvent se mettre dans le banc de registre ! Nous verrons le cas du ''program counter'' dans le chapitre suivant, qui porte sur l'unité de chargement. Dans ce cas, le séquenceur forge l'identifiant de registre de lui-même. Dans le cas des registres nommés, il ajoute quelques bits aux noms de registres. Pour les registres adressés implicitement, il forge l'identifiant à partir de rien.
[[File:Adressage du banc de registre - cas général.png|centre|vignette|upright=2|Adressage du banc de registre - cas général]]
Nous verrons plus bas que dans certains cas, le nom de registre ne suffit pas à adresser un registre dans un banc de registre. Dans ce cas, le séquenceur rajoute des bits, comme dans l'exemple précédent. Tout ce qu'il faut retenir est que l'identifiant de registre est forgé par le séquenceur, qui se base entre autres sur le nom de registre s'il est présent, sur l'instruction exécutée dans le cas d'un registre adressé implicitement.
===Les registres généraux===
Pour rappel, les registres généraux peuvent mémoriser des entiers, des adresses, ou toute autre donnée codée en binaire. Ils sont souvent séparés des registres flottants sur les architectures modernes. Les registres généraux sont rassemblés dans un banc de registre dédié, appelé le '''banc de registres généraux'''. Le banc de registres généraux est une mémoire multiport, avec au moins un port d'écriture et deux ports de lecture. La raison est que les instructions lisent deux opérandes dans les registres et enregistrent leur résultat dans des registres. Le tout se marie bien avec un banc de registre à deux de lecture (pour les opérandes) et un d'écriture (pour le résultat).
[[File:Banc de registre multiports.png|centre|vignette|upright=2|Banc de registre multiports.]]
L'interface exacte dépend de si l'architecture est une architecture 2 ou 3 adresses. Pour rappel, la différence entre les deux tient dans la manière dont on précise le registre où enregistrer le résultat d'une opération. Avec les architectures 2-adresses, on précise deux registres : le premier sert à la fois comme opérande et pour mémoriser le résultat, l'autre sert uniquement d'opérande. Un des registres est donc écrasé pour enregistrer le résultat. Sur les architecture 3-adresses, on précise trois registres : deux pour les opérandes, un pour le résultat.
Les architectures 2-adresses ont un banc de registre où on doit préciser deux "adresses", deux noms de registre. L'interface du banc de registre est donc la suivante :
[[File:Register File Medium.svg|centre|vignette|upright=1.5|Register File d'une architecture à 2-adresses]]
Les architectures 3-adresses doivent rajouter une troisième entrée pour préciser un troisième nom de registre. L'interface du banc de registre est donc la suivante :
[[File:Register File Large.svg|centre|vignette|upright=1.5|Register File d'une architecture à 3-adresses]]
Rien n'empêche d'utiliser plusieurs bancs de registres sur un processeur qui utilise des registres généraux. La raison est une question d'optimisation. Au-delà d'un certain nombre de registres, il devient difficile d'utiliser un seul gros banc de registres. Il faut alors scinder le banc de registres en plusieurs bancs de registres séparés. Le problème est qu'il faut prévoir de quoi échanger des données entre les bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue du langage machine. Sur d'autres processeurs, les transferts de données entre bancs de registres se font via une instruction spéciale, souvent appelée COPY.
===Les registres flottants : banc de registre séparé ou unifié===
Passons maintenant aux registres flottants. Intuitivement, on a des registres séparés pour les entiers et les flottants. Il est alors plus simple d'utiliser un banc de registres séparé pour les nombres flottants, à côté d'un banc de registre entiers. L'avantage est que les nombres flottants et entiers n'ont pas forcément la même taille, ce qui se marie bien avec deux bancs de registres, où la taille des registres est différente dans les deux bancs.
Mais d'autres processeurs utilisent un seul '''banc de registres unifié''', qui regroupe tous les registres de données, qu'ils soient entier ou flottants. Par exemple, c'est le cas des Pentium Pro, Pentium II, Pentium III, ou des Pentium M : ces processeurs ont des registres séparés pour les flottants et les entiers, mais ils sont regroupés dans un seul banc de registres. Avec cette organisation, un registre flottant et un registre entier peuvent avoir le même nom de registre en langage machine, mais l'adresse envoyée au banc de registres ne doit pas être la même : le séquenceur ajoute des bits au nom de registre pour former l'adresse finale.
[[File:Désambiguïsation de registres sur un banc de registres unifié.png|centre|vignette|upright=2|Désambiguïsation de registres sur un banc de registres unifié.]]
===Le registre d'état===
Le registre d'état fait souvent bande à part et n'est pas placé dans un banc de registres. En effet, le registre d'état est très lié à l'unité de calcul. Il reçoit des indicateurs/''flags'' provenant de la sortie de l'unité de calcul, et met ceux-ci à disposition du reste du processeur. Son entrée est connectée à l'unité de calcul, sa sortie est reliée au séquenceur et/ou au bus interne au processeur.
Le registre d'état est relié au séquenceur afin que celui-ci puisse gérer les instructions de branchement, qui ont parfois besoin de connaitre certains bits du registre d'état pour savoir si une condition a été remplie ou non. D'autres processeurs relient aussi le registre d'état au bus interne, ce qui permet de lire son contenu et de le copier dans un registre de données. Cela permet d'implémenter certaines instructions, notamment celles qui permettent de mémoriser le registre d'état dans un registre général.
[[File:Place du registre d'état dans le chemin de données.png|centre|vignette|upright=2|Place du registre d'état dans le chemin de données]]
L'ALU fournit une sortie différente pour chaque bit du registre d'état, la connexion du registre d'état est directe, comme indiqué dans le schéma suivant. Vous remarquerez que le bit de retenue est à la fois connecté à la sortie de l'ALU, mais aussi sur son entrée. Ainsi, le bit de retenue calculé par une opération peut être utilisé pour la suivante. Sans cela, diverses instructions comme les opérations ''add with carry'' ne seraient pas possibles.
[[File:AluStatusRegister.svg|centre|vignette|upright=2|Registre d'état et unit de calcul.]]
Il est techniquement possible de mettre le registre d'état dans le banc de registre, pour économiser un registre. La principale difficulté est que les instructions doivent faire deux écritures dans le banc de registre : une pour le registre de destination, une pour le registre d'état. Soit on utilise deux ports d'écriture, soit on fait les deux écritures l'une après l'autre. Dans les deux cas, le cout en performances et en transistors n'en vaut pas le cout. D'ailleurs, je ne connais aucun processeur qui utilise cette technique.
Il faut noter que le registre d'état n'existe pas forcément en tant que tel dans le processeur. Quelques processeurs, dont le 8086 d'Intel, utilisent des bascules dispersées dans le processeur au lieu d'un vrai registre d'état. Les bascules dispersées mémorisent chacune un bit du registre d'état et sont placées là où elles sont le plus utile. Les bascules utilisées pour les branchements sont proches du séquenceur, le bascules pour les bits de retenue sont placées proche de l'ALU, etc.
===Les registres à prédicats===
Les registres à prédicats remplacent le registre d'état sur certains processeurs. Pour rappel, les registres à prédicat sont des registres de 1 bit qui mémorisent les résultats des comparaisons et instructions de test. Ils sont nommés/numérotés, mais les numéros en question sont distincts de ceux utilisés pour les registres généraux.
Ils sont placés à part, dans un banc de registres séparé. Le banc de registres à prédicats a une entrée de 1 bit connectée à l'ALU et une sortie de un bit connectée au séquenceur. Le banc de registres à prédicats est parfois relié à une unité de calcul spécialisée dans les conditions/instructions de test. Pour rappel, certaines instructions permettent de faire un ET, un OU, un XOR entre deux registres à prédicats. Pour cela, l'unité de calcul dédiée aux conditions peut lire les registres à prédicats, pour combiner le contenu de plusieurs d'entre eux.
[[File:Banc de registre pour les registres à prédicats.png|centre|vignette|upright=2|Banc de registre pour les registres à prédicats]]
===Les registres dédiés aux interruptions===
Dans le chapitre sur les registres, nous avions vu que certains processeurs dupliquaient leurs registres architecturaux, pour accélérer les interruptions ou les appels de fonction. Dans le cas qui va nous intéresser, les interruptions avaient accès à leurs propres registres, séparés des registres architecturaux. Les processeurs de ce type ont deux ensembles de registres identiques : un dédié aux interruptions, un autre pour les programmes normaux. Les registres dans les deux ensembles ont les mêmes noms, mais le processeur choisit le bon ensemble suivant s'il est dans une interruption ou non. Si on peut utiliser deux bancs de registres séparés, il est aussi possible d'utiliser un banc de registre unifié pour les deux.
Sur certains processeurs, le banc de registre est dupliqué en plusieurs exemplaires. La technique est utilisée pour les interruptions. Certains processeurs ont deux ensembles de registres identiques : un dédié aux interruptions, un autre pour les programmes normaux. Les registres dans les deux ensembles ont les mêmes noms, mais le processeur choisit le bon ensemble suivant s'il est dans une interruption ou non. On peut utiliser deux bancs de registres séparés, un pour les interruptions, et un pour les programmes.
Sur d'autres processeurs, on utilise un banc de registre unifié pour les deux ensembles de registres. Les registres pour les interruptions sont dans les adresses hautes, les registres pour les programmes dans les adresses basses. Le choix entre les deux est réalisé par un bit qui indique si on est dans une interruption ou non, disponible dans une bascule du processeur. Appelons là la bascule I.
===Le fenêtrage de registres===
[[File:Fenetre de registres.png|vignette|upright=1|Fenêtre de registres.]]
Le '''fenêtrage de registres''' fait que chaque fonction a accès à son propre ensemble de registres, sa propre fenêtre de registres. Là encore, cette technique duplique chaque registre architectural en plusieurs exemplaires qui portent le même nom. Chaque ensemble de registres architecturaux forme une fenêtre de registre, qui contient autant de registres qu'il y a de registres architecturaux. Lorsqu'une fonction s’exécute, elle se réserve une fenêtre inutilisée, et peut utiliser les registres de la fenêtre comme bon lui semble : une fonction manipule le registre architectural de la fenêtre réservée, mais pas les registres avec le même nom dans les autres fenêtres.
Il peut s'implémenter soit avec un banc de registres unifié, soit avec un banc de registre par fenêtre de registres.
Il est possible d'utiliser des bancs de registres dupliqués pour le fenêtrage de registres. Chaque fenêtre de registre a son propre banc de registres. Le choix entre le banc de registre à utiliser est fait par un registre qui mémorise le numéro de la fenêtre en cours. Ce registre commande un multiplexeur qui permet de choisir le banc de registre adéquat.
[[File:Fenêtrage de registres au niveau du banc de registres.png|vignette|Fenêtrage de registres au niveau du banc de registres.]]
L'utilisation d'un banc de registres unifié permet d'implémenter facilement le fenêtrage de registres. Il suffit pour cela de regrouper tous les registres des différentes fenêtres dans un seul banc de registres. Il suffit de faire comme vu au-dessus : rajouter des bits au nom de registre pour faire la différence entre les fenêtres. Cela implique de se souvenir dans quelle fenêtre de registre on est actuellement, cette information étant mémorisée dans un registre qui stocke le numéro de la fenêtre courante. Pour changer de fenêtre, il suffit de modifier le contenu de ce registre lors d'un appel ou retour de fonction avec un petit circuit combinatoire. Bien sûr, il faut aussi prendre en compte le cas où ce registre déborde, ce qui demande d'ajouter des circuits pour gérer la situation.
[[File:Désambiguïsation des fenêtres de registres.png|centre|vignette|upright=2|Désambiguïsation des fenêtres de registres.]]
==L'interface de communication avec la mémoire==
L''''interface avec la mémoire''' est, comme son nom l'indique, des circuits qui servent d'intermédiaire entre le bus mémoire et le processeur. Elle est parfois appelée l'unité mémoire, l'unité d'accès mémoire, la ''load-store unit'', et j'en oublie.
[[File:Unité de communication avec la mémoire, de type simple port.png|centre|vignette|upright=2|Unité de communication avec la mémoire, de type simple port.]]
Sur certains processeurs, elle gère les mémoires multiport.
[[File:Unité de communication avec la mémoire, de type multiport.png|centre|vignette|upright=2|Unité de communication avec la mémoire, de type multiport.]]
===Les registres d'interfaçage mémoire===
L'interface mémoire se résume le plus souvent à des '''registres d’interfaçage mémoire''', intercalés entre le bus mémoire et le chemin de données. Généralement, il y a au moins deux registres d’interfaçage mémoire : un registre relié au bus d'adresse, et autre relié au bus de données.
[[File:Registres d’interfaçage mémoire.png|centre|vignette|upright=2|Registres d’interfaçage mémoire.]]
Au lieu de lire ou écrire directement sur le bus, le processeur lit ou écrit dans ces registres, alors que l'unité d'accès mémoire s'occupe des échanges entre registres et bus mémoire. Lors d'une écriture, le processeur place l'adresse dans le registre d'interfaçage d'adresse, met la donnée à écrire dans le registre d'interfaçage de donnée, puis laisse l'unité d'accès mémoire faire son travail. Lors d'une lecture, il place l'adresse à lire sur le registre d'interfaçage d'adresse, il attend que la donnée soit lue, puis récupère la donnée dans le registre d'interfaçage de données.
L'avantage est que le processeur n'a pas à maintenir une donnée/adresse sur le bus durant tout un accès mémoire. Par exemple, prenons le cas où la mémoire met 15 cycles processeurs pour faire une lecture ou une écriture. Sans registres d'interfaçage mémoire, le processeur doit maintenir l'adresse durant 15 cycles, et aussi la donnée dans le cas d'une écriture. Avec ces registres, le processeur écrit dans les registres d'interfaçage mémoire au premier cycle, et passe les 14 cycles suivants à faire quelque chose d'autre. Par exemple, il faut faire un calcul en parallèle, envoyer des signaux de commande au banc de registre pour qu'il soit prêt une fois la donnée lue arrivée, etc. Cet avantage simplifie l'implémentation de certains modes d'adressage, comme on le verra à la fin du chapitre.
===L'unité de calcul d'adresse===
Les registres d'interfaçage sont presque toujours présents, mais le circuit que nous allons voir est complétement facultatif. Il s'agit d'une unité de calcul spécialisée dans les calculs d'adresse, dont nous avons parlé rapidement dans la section sur les ALU. Elle s'appelle l''''''Address generation unit''''', ou AGU. Elle est parfois séparée de l'interface mémoire proprement dit, et est alors considérée comme une unité de calcul à part, mais elle est généralement intimement liée à l'interface mémoire.
Elle sert pour certains modes d'adressage, qui demandent de combiner une adresse avec soit un indice, soit un décalage, plus rarement les deux. Les calculs d'adresse demandent de simplement incrémenter/décrémenter une adresse, de lui ajouter un indice (et de décaler les indices dans certains cas), mais guère plus. Pas besoin d'effectuer de multiplications, de divisions, ou d'autre opération plus complexe. Des décalages et des additions/soustractions suffisent. L'AGU est donc beaucoup plus simple qu'une ALU normale et se résume souvent à un vulgaire additionneur-soustracteur, éventuellement couplée à un décaleur pour multiplier les indices.
[[File:Unité d'accès mémoire avec unité de calcul dédiée.png|centre|vignette|upright=1.5|Unité d'accès mémoire avec unité de calcul dédiée]]
Le fait d'avoir une unité de calcul séparée pour les adresses peut s'expliquer pour plusieurs raisons. Sur les processeurs normaux, la raison est que cela simplifie un peu l'implémentation des modes d'adressage indirects. Sur les rares processeurs qui ont des registres séparés pour les adresses, un banc de registre dédié est réservé aux registres d'adresses, ce qui rend l'usage d'une unité de calcul d'adresse bien plus pratique. Une autre raison se manifestait sur les processeurs 8 bits : ils géraient des données de 8 bits, mais des adresses de 16 bits. Dans ce cas, le processeur avait une ALU simple de 16 bits pour les adresses, et une ALU complexe de 8 bits pour les données.
[[File:Unité d'accès mémoire avec registres d'adresse ou d'indice.png|centre|vignette|upright=2|Unité d'accès mémoire avec registres d'adresse ou d'indice]]
===La gestion de l'alignement et du boutisme===
L'interface mémoire gère les accès mémoire non-alignés, à cheval sur deux mots mémoire (rappelez-vous le chapitre sur l'alignement mémoire). Elle détecte les accès mémoire non-alignés et réagit en conséquence. Dans le cas où les accès non-alignés sont interdits, elle lève une exception matérielle. Dans le cas où ils sont autorisés, elle les gère automatiquement, à savoir qu'elle charge deux mots mémoire et les combine entre eux pour donner le résultat final. Dans les deux cas, cela demande d'ajouter des circuits de détection des accès non-alignés, et éventuellement des circuits pour le double lecture/écriture.
Les circuits de détection des accès non-alignés sont très simples. Dans le cas où les adresses sont alignées sur une puissance de deux (cas le plus courant), il suffit de vérifier les bits de poids faible de l'adresse à lire. Prenons l'exemple d'un processeur avec des adresses codées sur 64 bits, avec des mots mémoire de 32 bits, alignés sur 32 bits (4 octets). Un mot mémoire contient 4 octets, les contraintes d'alignement font que les adresses autorisées sont des multiples de 4. En conséquence, les 2 bits de poids faible d'une adresse valide sont censés être à 0. En vérifiant la valeur de ces deux bits, on détecte facilement les accès non-alignés.
En clair, détecter les accès non-alignés demande de tester si les bits de poids faibles adéquats sont à 0. Il suffit donc d'un circuit de comparaison avec zéro; qui est une simple porte OU. Cette porte OU génère un bit qui indique si l'accès testé est aligné ou non : 1 si l'accès est non-aligné, 0 sinon. Le signal peut être transmis au séquenceur pour générer une exception matérielle, ou utilisé dans l'unité d'accès mémoire pour la double lecture/écriture.
La gestion automatique des accès non-alignés est plus complexe. Dans ce cas, l'unité mémoire charge deux mots mémoire et les combine entre eux pour donner le résultat final. Charger deux mots mémoires consécutifs est assez simple, si le registre d'interfaçage est un compteur. L'accès initial charge le premier mot mémoire, puis l'adresse stockée dans le registre d'interfaçage est incrémentée pour démarrer un second accès. Le circuit pour combiner deux mots mémoire contient des registres, des circuits de décalage, des multiplexeurs.
===Le rafraichissement mémoire optimisé et le contrôleur mémoire intégré===
Depuis les années 80, les processeurs sont souvent combinés avec une mémoire principale de type DRAM. De telles mémoires doivent être rafraichies régulièrement pour ne pas perdre de données. Le rafraichissement se fait généralement adresse par adresse, ou ligne par ligne (les lignes sont des super-bytes internes à la DRAM). Le rafraichissement est en théorie géré par le contrôleur mémoire installé sur la carte mère. Mais au tout début de l'informatique, du temps des processeurs 8 bits, le rafraichissement mémoire était géré directement par le processeur.
Si quelques processeurs géraient le rafraichissement mémoire avec des interruptions, d'autres processeurs disposaient d’optimisations pour optimiser le rafraichissement mémoire. Divers processeurs implémentaient de quoi faciliter le rafraichissement par adresse. Par exemple, le processeur Zilog Z80 contenait un compteur de ligne, un registre qui contenait le numéro de la prochaine ligne à rafraichir. Il était incrémenté à chaque rafraichissement mémoire, automatiquement, par le processeur lui-même. Un ''timer'' interne permettait de savoir quand rafraichir la mémoire : quand ce ''timer'' atteignait 0, une commande de rafraichissement était envoyée à la mémoire, et le ''timer'' était ''reset''. Et tout cela était intégré à l'unité d'accès mémoire.
Depuis les années 2000, les processeurs modernes ont un contrôleur mémoire DRAM intégré directement dans le processeur. Ce qui fait qu'ils gèrent non seulement le rafraichissement, mais aussi d'autres fonctions bien pus complexes.
==Le chemin de données et son réseau d'interconnexions==
Nous venons de voir que le chemin de données contient une unité de calcul (parfois plusieurs), des registres isolés, un banc de registre, une unité mémoire. Le tout est chapeauté par une unité de contrôle qui commande le chemin de données, qui fera l'objet des prochains chapitres. Mais il faut maintenant relier registres, ALU et unité mémoire pour que l'ensemble fonctionne. Pour cela, diverses interconnexions internes au processeur se chargent de relier le tout.
Sur les anciens processeurs, les interconnexions sont assez simples et se résument à un ou deux '''bus internes au processeur''', reliés au bus mémoire. C'était la norme sur des architectures assez ancienne, qu'on n'a pas encore vu à ce point du cours, appelées les architectures à accumulateur et à pile. Mais ce n'est plus la solution utilisée actuellement. De nos jours, le réseaux d'interconnexion intra-processeur est un ensemble de connexions point à point entre ALU/registres/unité mémoire. Et paradoxalement, cela rend plus facile de comprendre ce réseau d'interconnexion.
===Introduction propédeutique : l'implémentation des modes d'adressage principaux===
L'organisation interne du processeur dépend fortement des modes d'adressage supportés. Pour simplifier les explications, nous allons séparer les modes d'adressage qui gèrent les pointeurs et les autres. Suivant que le processeur supporte les pointeurs ou non, l'organisation des bus interne est légèrement différente. La différence se voit sur les connexions avec le bus d'adresse et de données.
Tout processeur gère au minimum le '''mode d'adressage absolu''', où l'adresse est intégrée à l'instruction. Le séquenceur extrait l'adresse mémoire de l'instruction, et l'envoie sur le bus d'adresse. Pour cela, le séquenceur est relié au bus d'adresse, le chemin de donnée est relié au bus de données. Le chemin de donnée n'est pas connecté au bus d'adresse, il n'y a pas d'autres connexions.
[[File:Chemin de données sans support des pointeurs.png|centre|vignette|upright=2|Chemin de données sans support des pointeurs]]
Le '''support des pointeurs''' demande d'intégrer des modes d'adressage dédiés : l'adressage indirect à registre, l'adresse base + indice, et les autres. Les pointeurs sont stockés dans le banc de registre et sont modifiés par l'unité de calcul. Pour supporter les pointeurs, le chemin de données est connecté sur le bus d'adresse avec le séquenceur. Suivant le mode d'adressage, le bus d'adresse est relié soit au chemin de données, soit au séquenceur.
[[File:Chemin de données avec support des pointeurs.png|centre|vignette|upright=2|Chemin de données avec support des pointeurs]]
Pour terminer, il faut parler des instructions de '''copie mémoire vers mémoire''', qui copient une donnée d'une adresse mémoire vers une autre. Elles ne se passent pas vraiment dans le chemin de données, mais se passent purement au niveau des registres d’interfaçage. L'usage d'un registre d’interfaçage unique permet d'implémenter ces instructions très facilement. Elle se fait en deux étapes : on copie la donnée dans le registre d’interfaçage, on l'écrit en mémoire RAM. L'adresse envoyée sur le bus d'adresse n'est pas la même lors des deux étapes.
===Le banc de registre est multi-port, pour gérer nativement les opérations dyadiques===
Les architectures RISC et CISC incorporent un banc de registre, qui est connecté aux unités de calcul et au bus mémoire. Et ce banc de registre peut être mono-port ou multiport. S'il a existé d'anciennes architectures utilisant un banc de registre mono-port, elles sont actuellement obsolètes. Nous les aborderons dans un chapitre dédié aux architectures dites canoniques, mais nous pouvons les laisser de côté pour le moment. De nos jours, tous les processeurs utilisent un banc de registre multi-port.
[[File:Chemin de données minimal d'une architecture LOAD-STORE (sans MOV inter-registres).png|centre|vignette|upright=2|Chemin de données minimal d'une architecture LOAD-STORE (sans MOV inter-registres)]]
Le banc de registre multiport est optimisé pour les opérations dyadiques. Il dispose précisément de deux ports de lecture et d'un port d'écriture pour l'écriture. Un port de lecture par opérande et le port d'écriture pour enregistrer le résultat. En clair, le processeur peut lire deux opérandes et écrire un résultat en un seul cycle d'horloge. L'avantage est que les opérations simples ne nécessitent qu'une micro-opération, pas plus.
[[File:ALU data paths.svg|centre|vignette|upright=1.5|Processeur LOAD-STORE avec un banc de registre multiport, avec les trois ports mis en évidence.]]
===Une architecture LOAD-STORE basique, avec adressage absolu===
Voyons maintenant comment l'implémentation d'une architecture RISC très simple, qui ne supporte pas les adressages pour les pointeurs, juste les adressages inhérent (à registres) et absolu (par adresse mémoire). Les instructions LOAD et STORE utilisent l'adressage absolu, géré par le séquenceur, reste à gérer l'échange entre banc de registres et bus de données. Une lecture LOAD relie le bus de données au port d'écriture du banc de registres, alors que l'écriture relie le bus au port de lecture du banc de registre. Pour cela, il faut ajouter des multiplexeurs sur les chemins existants, comme illustré par le schéma ci-dessous.
[[File:Bus interne au processeur sur archi LOAD STORE avec banc de registres multiport.png|centre|vignette|upright=2|Organisation interne d'une architecture LOAD STORE avec banc de registres multiport. Nous n'avons pas représenté les signaux de commandes envoyés par le séquenceur au chemin de données.]]
Ajoutons ensuite les instructions de copie entre registres, souvent appelées instruction COPY ou MOV. Elles existent sur la plupart des architectures LOAD-STORE. Une première solution boucle l'entrée du banc de registres sur son entrée, ce qui ne sert que pour les copies de registres.
[[File:Chemin de données d'une architecture LOAD-STORE.png|centre|vignette|upright=2|Chemin de données d'une architecture LOAD-STORE]]
Mais il existe une seconde solution, qui ne demande pas de modifier le chemin de données. Il est possible de faire passer les copies de données entre registres par l'ALU. Lors de ces copies, l'ALU une opération ''Pass through'', à savoir qu'elle recopie une des opérandes sur sa sortie. Le fait qu'une ALU puisse effectuer une opération ''Pass through'' permet de fortement simplifier le chemin de donnée, dans le sens où cela permet d'économiser des multiplexeurs. Mais nous verrons cela sous peu. D'ailleurs, dans la suite du chapitre, nous allons partir du principe que les copies entre registres passent par l'ALU, afin de simplifier les schémas.
===L'ajout des modes d'adressage indirects à registre pour les pointeurs===
Passons maintenant à l'implémentation des modes d'adressages pour les pointeurs. Avec eux, l'adresse mémoire à lire/écrire n'est pas intégrée dans une instruction, mais est soit dans un registre, soit calculée par l'ALU.
Le premier mode d'adressage de ce type est le mode d'adressage indirect à registre, où l'adresse à lire/écrire est dans un registre. L'implémenter demande donc de connecter la sortie du banc de registres au bus d'adresse. Il suffit d'ajouter un MUX en sortie d'un port de lecture.
[[File:Chemin de données à trois bus.png|centre|vignette|upright=2|Chemin de données à trois bus.]]
Le mode d'adressage base + indice est un mode d'adressage où l'adresse à lire/écrire est calculée à partir d'une adresse et d'un indice, tous deux présents dans un registre. Le calcul de l'adresse implique au minimum une addition et donc l'ALU. Dans ce cas, on doit connecter la sortie de l'unité de calcul au bus d'adresse.
[[File:Bus avec adressage base+index.png|centre|vignette|upright=2|Bus avec adressage base+index]]
Le chemin de données précédent gère aussi le mode d'adressage indirect avec pré-décrément. Pour rappel, ce mode d'adressage est une variante du mode d'adressage indirect, qui utilise une pointeur/adresse stocké dans un registre. La différence est que ce pointeur est décrémenté avant d'être envoyé sur le bus d'adresse. L'implémentation matérielle est la même que pour le mode Base + Indice : l'adresse est lue depuis les registres, décrémentée dans l'ALU, et envoyée sur le bus d'adresse.
Le schéma précédent montre que le bus d'adresse est connecté à un MUX avant l'ALU et un autre MUX après. Mais il est possible de se passer du premier MUX, utilisé pour le mode d'adressage indirect à registre. La condition est que l'ALU supporte l'opération ''pass through'', un NOP, qui recopie une opérande sur sa sortie. L'ALU fera une opération NOP pour le mode d'adressage indirect à registre, un calcul d'adresse pour le mode d'adressage base + indice. Par contre, faire ainsi rendra l'adressage indirect légèrement plus lent, vu que le temps de passage dans l'ALU sera compté.
[[File:Bus avec adressage indirect.png|centre|vignette|upright=2|Bus avec adressages pour les pointeurs, simplifié.]]
Dans ce qui va suivre, nous allons partir du principe que le processeur est implémenté en suivant le schéma précédent, afin d'avoir des schéma plus lisibles.
===L'adressage immédiat et les modes d'adressages exotiques===
Passons maintenant au mode d’adressage immédiat, qui permet de préciser une constante dans une instruction directement. La constante est extraite de l'instruction par le séquenceur, puis insérée au bon endroit dans le chemin de données. Pour les opérations arithmétiques/logiques/branchements, il faut insérer la constante extraite sur l'entrée de l'ALU. Sur certains processeurs, la constante peut être négative et doit alors subir une extension de signe dans un circuit spécialisé.
[[File:Chemin de données - Adressage immédiat avec extension de signe.png|centre|vignette|upright=2|Chemin de données - Adressage immédiat avec extension de signe.]]
L'implémentation précédente gère aussi les modes d'adressage base + décalage et absolu indexé. Pour rappel, le premier ajoute une constante à une adresse prise dans les registres, le second prend une adresse constante et lui ajoute un indice pris dans les registres. Dans les deux cas, on lit un registre, extrait une constante/adresse de l’instruction, additionne les deux dans l'ALU, avant d'envoyer le résultat sur le bus d'adresse. La seule difficulté est de désactiver l'extension de signe pour les adresses.
Le mode d'adressage absolu peut être traité de la même manière, si l'ALU est capable de faire des NOPs. L'adresse est insérée au même endroit que pour le mode d'adressage immédiat, parcours l'unité de calcul inchangée parce que NOP, et termine sur le bus d'adresse.
[[File:Chemin de données avec une ALU capable de faire des NOP.png|centre|vignette|upright=2|Chemin de données avec adressage immédiat étendu pour gérer des adresses.]]
Passons maintenant au cas particulier d'une instruction MOV qui copie une constante dans un registre. Il n'y a rien à faire si l'unité de calcul est capable d'effectuer une opération NOP/''pass through''. Pour charger une constante dans un registre, l'ALU est configurée pour faire un NOP, la constante traverse l'ALU et se retrouve dans les registres. Si l'ALU ne gère pas les NOP, la constante doit être envoyée sur l'entrée d'écriture du banc de registres, à travers un MUX dédié.
[[File:Implémentation de l'adressage immédiat dans le chemin de données.png|centre|vignette|upright=2|Implémentation de l'adressage immédiat dans le chemin de données]]
===Les architectures CISC : les opérations ''load-op''===
Tout ce qu'on a vu précédemment porte sur les processeurs de type LOAD-STORE, souvent confondus avec les processeurs de type RISC, où les accès mémoire sont séparés des instructions utilisant l'ALU. Il est maintenant temps de voir les processeurs CISC, qui gèrent des instructions ''load-op'', qui peuvent lire une opérande depuis la mémoire.
L'implémentation des opérations ''load-op'' relie le bus de donnée directement sur une entrée de l'unité de calcul, en utilisant encore une fois un multiplexeur. L'implémentation parait simple, mais c'est parce que toute la complexité est déportée dans le séquenceur. C'est lui qui se charge de détecter quand la lecture de l'opérande est terminée, quand l'opérande est disponible.
Les instructions ''load-op'' s'exécutent en plusieurs étapes, en plusieurs micro-opérations. Il y a typiquement une étape pour l'opérande à lire en mémoire et une étape de calcul. L'usage d'un registre d’interfaçage permet d'implémenter les instructions ''load-op'' très facilement. Une opération ''load-op'' charge l'opérande en mémoire dans un registre d’interfaçage, puis relier ce registre d’interfaçage sur une des entrées de l'ALU. Un simple multiplexeur suffit pour implémenter le tout, en plus des modifications adéquates du séquenceur.
[[File:Chemin de données d'un CPU CISC avec lecture des opérandes en mémoire.png|centre|vignette|upright=2|Chemin de données d'un CPU CISC avec lecture des opérandes en mémoire]]
Supporter les instructions multi-accès (qui font plusieurs accès mémoire) ne modifie pas fondamentalement le réseau d'interconnexion, ni le chemin de données La raison est que supporter les instructions multi-accès se fait au niveau du séquenceur. En réalité, les accès mémoire se font en série, l'un après l'autre, sous la commande du séquenceur qui émet plusieurs micro-opérations mémoire consécutives. Les données lues sont placées dans des registres d’interactivement mémoire, ce qui demande d'ajouter des registres d’interfaçage mémoire en plus.
==Annexe : le cas particulier du pointeur de pile==
Le pointeur de pile est un registre un peu particulier. Il peut être placé dans le chemin de données ou dans le séquenceur, voire dans l'unité de chargement, tout dépend du processeur. Tout dépend de si le pointeur de pile gère une pile d'adresses de retour ou une pile d'appel.
===Le pointeur de pile non-adressable explicitement===
Avec une pile d'adresse de retour, le pointeur de pile n'est pas adressable explicitement, il est juste adressé implicitement par des instructions d'appel de fonction CALL et des instructions de retour de fonction RET. Le pointeur de pile est alors juste incrémenté ou décrémenté par un pas constant, il ne subit pas d'autres opérations, son adressage est implicite. Il est juste incrémenté/décrémenté par pas constants, qui sont fournis par le séquenceur. Il n'y a pas besoin de le relier au chemin de données, vu qu'il n'échange pas de données avec les autres registres. Il y a alors plusieurs solutions, mais la plus simple est de placer le pointeur de pile dans le séquenceur et de l'incrémenter par un incrémenteur dédié.
Quelques processeurs simples disposent d'une pile d'appel très limitée, où le pointeur de pile n'est pas adressable explicitement. Il est adressé implicitement par les instruction CALL, RET, mais aussi PUSH et POP, mais aucune autre instruction ne permet cela. Là encore, le pointeur de pile ne communique pas avec les autres registres. Il est juste incrémenté/décrémenté par pas constants, qui sont fournis par le séquenceur. Là encore, le plus simple est de placer le pointeur de pile dans le séquenceur et de l'incrémenter par un incrémenteur dédié.
Dans les deux cas, le pointeur de pile est placé dans l'unité de contrôle, le séquenceur, et est associé à un incrémenteur dédié. Il se trouve que cet incrémenteur est souvent partagé avec le ''program counter''. En effet, les deux sont des adresses mémoire, qui sont incrémentées et décrémentées par pas constants, ne subissent pas d'autres opérations (si ce n'est des branchements, mais passons). Les ressemblances sont suffisantes pour fusionner les deux circuits. Ils peuvent donc avoir un '''incrémenteur partagé'''.
L'incrémenteur en question est donc partagé entre pointeur de pile, ''program counter'' et quelques autres registres similaires. Par exemple, le Z80 intégrait un registre pour le rafraichissement mémoire, qui était réalisé par le CPU à l'époque. Ce registre contenait la prochaine adresse mémoire à rafraichir, et était incrémenté à chaque rafraichissement d'une adresse. Et il était lui aussi intégré au séquenceur et incrémenté par l'incrémenteur partagé.
[[File:Organisation interne d'une architecture à pile.png|centre|vignette|upright=2|Organisation interne d'une architecture à pile]]
===Le pointeur de pile adressable explicitement===
Maintenant, étudions le cas d'une pile d'appel, précisément d'une pile d'appel avec des cadres de pile de taille variable. Sous ces conditions, le pointeur de pile est un registre adressable, avec un nom/numéro de registre dédié. Tel est par exemple le cas des processeurs x86 avec le registre ESP (''Extended Stack Pointer''). 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 calculs d'adresse, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres.
Dans ce cas, la meilleure solution est de placer le pointeur de pile dans le banc de registre généraux, avec les autres registres entiers. En faisant cela, la manipulation du pointeur de pile est faite par l'unité de calcul entière, pas besoin d'utiliser un incrémenteur dédiée. Il a existé des processeurs qui mettaient le pointeur de pile dans le banc de registre, mais l'incrémentaient avec un incrémenteur dédié, mais nous les verrons dans le chapitre sur les architectures à accumulateur. La raison est que sur les processeurs concernés, les adresses ne faisaient pas la même taille que les données : c'était des processeurs 8 bits, qui géraient des adresses de 16 bits.
==Annexe : l'implémentation du système d'''aliasing'' des registres des CPU x86==
Il y a quelques chapitres, nous avions parlé du système d'''aliasing'' des registres des CPU x86. Pour rappel, il permet de donner plusieurs noms de registre pour un même registre. Plus précisément, pour un registre 64 bits, le registre complet aura un nom de registre, les 32 bits de poids faible auront leur nom de registre dédié, idem pour les 16 bits de poids faible, etc. Il est possible de faire des calculs sur ces moitiés/quarts/huitièmes de registres sans problème.
===L'''aliasing'' du 8086, pour les registres 16 bits===
[[File:Register 8086.PNG|vignette|Register 8086]]
L'implémentation de l'''aliasing'' est apparue sur les premiers CPU Intel 16 bits, notamment le 8086. En tout, ils avaient quatre registres généraux 16 bits : AX, BX, CX et DX. Ces quatre registres 16 bits étaient coupés en deux octets, chacun adressable. Par exemple, le registre AX était coupé en deux octets nommés AH et AL, chacun ayant son propre nom/numéro de registre. Les instructions d'addition/soustraction pouvaient manipuler le registre AL, ou le registre AH, ce qui modifiait les 8 bits de poids faible ou fort selon le registre choisit.
Le banc de registre ne gére que 4 registres de 16 bits, à savoir AX, BX, CX et DX. Lors d'une lecture d'un registre 8 bits, le registre 16 bit entier est lu depuis le banc de registre, mais les bits inutiles sont ignorés. Par contre, l'écriture peut se faire soit avec 16 bits d'un coup, soit pour seulement un octet. Le port d'écriture du banc de registre peut être configuré de manière à autoriser l'écriture soit sur les 16 bits du registre, soit seulement sur les 8 bits de poids faible, soit écrire dans les 8 bits de poids fort.
[[File:Port d'écriture du banc de registre du 8086.png|centre|vignette|upright=2.5|Port d'écriture du banc de registre du 8086]]
Une opération sur un registre 8 bits se passe comme suit. Premièrement, on lit le registre 16 bits complet depuis le banc de registre. Si l'on a sélectionné l'octet de poids faible, il ne se passe rien de particulier, l'opérande 16 bits est envoyée directement à l'ALU. Mais si on a sélectionné l'octet de poids fort, la valeur lue est décalée de 7 rangs pour atterrir dans les 8 octets de poids faible. Ensuite, l'unité de calcul fait un calcul avec cet opérande, un calcul 16 bits tout ce qu'il y a de plus classique. Troisièmement, le résultat est enregistré dans le banc de registre, en le configurant convenablement. La configuration précise s'il faut enregistrer le résultat dans un registre 16 bits, soit seulement dans l'octet de poids faible/fort.
Afin de simplifier le câblage, les 16 bits des registres AX/BX/CX/DX sont entrelacés d'une manière un peu particulière. Intuitivement, on s'attend à ce que les bits soient physiquement dans le même ordre que dans le registre : le bit 0 est placé à côté du bit 1, suivi par le bit 2, etc. Mais à la place, l'octet de poids fort et de poids faible sont mélangés. Deux bits consécutifs appartiennent à deux octets différents. Le tout est décrit dans le tableau ci-dessous.
{|class="wikitable"
|-
! Registre 16 bits normal
| class="f_bleu" | 15
| class="f_bleu" | 14
| class="f_bleu" | 13
| class="f_bleu" | 12
| class="f_bleu" | 11
| class="f_bleu" | 10
| class="f_bleu" | 9
| class="f_bleu" | 8
| class="f_rouge" | 7
| class="f_rouge" | 6
| class="f_rouge" | 5
| class="f_rouge" | 4
| class="f_rouge" | 3
| class="f_rouge" | 2
| class="f_rouge" | 1
| class="f_rouge" | 0
|-
! Registre 16 bits du 8086
| class="f_bleu" | 15
| class="f_rouge" | 7
| class="f_bleu" | 14
| class="f_rouge" | 6
| class="f_bleu" | 13
| class="f_rouge" | 5
| class="f_bleu" | 12
| class="f_rouge" | 4
| class="f_bleu" | 11
| class="f_rouge" | 3
| class="f_bleu" | 10
| class="f_rouge" | 2
| class="f_bleu" | 9
| class="f_rouge" | 1
| class="f_bleu" | 8
| class="f_rouge" | 0
|}
En faisant cela, le décaleur en entrée de l'ALU est bien plus simple. Il y a 8 multiplexeurs, mais le câblage est bien plus simple. Par contre, en sortie de l'ALU, il faut remettre les bits du résultat dans l'ordre adéquat, celui du registre 8086. Pour cela, les interconnexions sur le port d'écriture sont conçues pour. Il faut juste mettre les fils de sortie de l'ALU sur la bonne entrée, par besoin de multiplexeurs.
===L'''aliasing'' sur les processeurs x86 32/64 bits===
Les processeurs x86 32 et 64 bits ont un système d'''aliasing'' qui complète le système précédent. Les processeurs 32 bits étendent les registres 16 bits existants à 32 bits. Pour ce faire, le registre 32 bit a un nouveau nom de registre, distincts du nom de registre utilisé pour l'ancien registre 16 bits. Il est possible d'adresser les 16 bits de poids faible de ce registre, avec le même nom de registre que celui utilisé pour le registre 16 sur les processeurs d'avant. Même chose avec les processeurs 64, avec l'ajout d'un nouveau nom de registre pour adresser un registre de 64 bit complet.
En soit, implémenter ce système n'est pas compliqué. Prenons le cas du registre RAX (64 bits), et de ses subdivisions nommées EAX (32 bits), AX (16 bits). À l'intérieur du banc de registre, il n'y a que le registre RAX. Le banc de registre ne comprend qu'un seul nom de registre : RAX. Les subdivisions EAX et AX n'existent qu'au niveau de l'écriture dans le banc de registre. L'écriture dans le banc de registre est configurable, de manière à ne modifier que les bits adéquats. Le résultat d'un calcul de l'ALU fait 64 bits, il est envoyé sur le port d'écriture. À ce niveau, soit les 64 bits sont écrits dans le registre, soit seulement les 32/16 bits de poids faible. Le système du 8086 est préservé pour les écritures dans les 16 bits de poids faible.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les composants d'un processeur
| prevText=Les composants d'un processeur
| next=L'unité de chargement et le program counter
| nextText=L'unité de chargement et le program counter
}}
</noinclude>
6drkik0deogijs53teo9fy2a7octc81
Formation musicale/Harmonie
0
75583
745862
742380
2025-07-03T11:11:03Z
Cdang
1202
/* Principaux accords */ synoptiques classique/jazz
745862
wikitext
text/x-wiki
{{Bases de solfège}}
<span style="font-size:25px;">6. Harmonie</span>
L'harmonie désigne les notes jouées en même temps, soit plusieurs instruments jouant chacun une note, soit un instrument jouant un accord (instrument dit polyphonique).
== Première approche ==
L'exemple le plus simple d'harmonie est sans doute la chanson en canon : c'est un chant polyphonique, c'est-à-dire à plusieurs voix, chaque voix chantant la même chose en décalé. Prenons par exemple ''Vent frais, vent du matin'' (la version originale est ''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609) :
[[Fichier:Vent frais vent du matin.svg|class=transparent|center|Partition de ''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]]
[[Fichier:Vent frais vent du matin.midi|vignette|''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]]
nous voyons que les voix se superposent de manière « harmonieuse ». Les notes de chaque voix se correspondent point par point (avec un retard), c'est donc un type d'harmonie polyphonique appelé « contrepoint ».
Considérons la première note de la mesure 6 pour chaque voix. Nous avons la superposition des notes ''ré''-''fa''-''la'' (du grave vers l'aigu) ; la superposition de notes jouées ou chantées ensembles s'appelle un accord. Cet accord ''ré''-''fa''-''la'' porte le nom « d'accord parfait de ''ré'' mineur » :
* « ''ré'' » car la note fondamentale est un ''ré'' ;
* « parfait » car il est l'association d'une tierce, ''ré''-''fa'', et d'une quinte juste, ''ré''-''la'' ;
* « mineur » car le premier intervalle, ''ré''-''fa'', est une tierce mineure.
Considérons maintenant un chant accompagné au piano. La piano peut jouer plusieurs notes en même temps, il peut jouer des accords.
[[Fichier:Au clair de le lune chant et piano.svg|class=transparent|center|Deux premières mesure d’Au clair de la lune.]]
[[Fichier:Au clair de le lune chant et piano.midi|vignette|Deux premières mesure d’Au clair de la lune.]]
L'accord, les notes à jouer simultanément, sont écrites « en colonne ». Lorsqu'on les énonce, on les lit de bas en haut mais le pianiste les joue en pressant les touches du clavier en même temps, de manière « plaquée ».
Le premier accord est composé des notes ''do''-''mi''-''sol'' ; il est appelé « accord parfait de ''do'' majeur » car la note fondamentale est ''do'', qu'il est l'association d'une tierce et d'une quinte juste et que le premier intervalle, ''do''-''mi'', est une tierce majeure.
== Consonance et dissonance ==
Les notions de consonance et de dissonance sont culturelles et changent selon l'époque. Nous pouvons néanmoins noter que :
* l'accord de seconde, et son renversement la septième, créent des battements, les notes « frottent », c'est un intervalle harmonique dissonant ; mais dans le cas de la septième, comme les notes sont éloignées, le frottement est moins perceptible ;
* les accords de tierce, quarte et quinte sonnent agréablement à l'oreille, ils sont consonants.
Dans la musique savante européenne, au début au du Moyen-Âge, seuls les accords de quarte et de quinte étaient considérés comme consonants, d'où leur qualification de « juste ». La tierce, et son renversement la sixte, étaient perçues comme dissonantes.
L'harmonie joue avec les consonances et les dissonances. Dans un premier temps, les harmonies dissonantes sont utilisées pour créer des tensions qui sont ensuite résolues, on utilise des successions « consonant-dissonant-consonant ». À force d'entendre des intervalles considérés comme dissonants, l'oreille s'habitue et certains finissent par être considérés comme consonants ; c'est ce qui est arrivé à la tierce et à la sixte à la fin du Moyen Âge avec le contrepoint.
Il faut ici aborder la notion d'harmonique des notes.
[[File:Harmoniques de do.svg|thumb|Les six premières harmoniques de ''do''.]]
Lorsque l'on joue une note, on entend d'autres notes plus aigües et plus faibles ; la note jouée est appelée la « fondamentale » et les notes plus aigües et plus faibles sont les « harmoniques ». C'est cette accumulation d'harmoniques qui donne la couleur au son, son timbre, qui fait qu'un piano ne sonne pas comme un violon. Par exemple, si l'on joue un ''do''<sup>1</sup><ref>Pour la notation des octaves, voir ''[[../Représentation_musicale#Désignation_des_octaves|Représentation musicale > Désignation des octaves]]''.</ref> (fondamentale), on entend le ''do''<sup>2</sup> (une octave plus aigu), puis un ''sol''<sup>2</sup>, puis encore un ''do''<sup>3</sup> plus aigu, puis un ''mi''<sup>3</sup>, puis encore un ''sol''<sup>3</sup>, puis un ''si''♭<sup>3</sup>…
Ainsi, puisque lorsque l'on joue un ''do'' on entend aussi un ''sol'' très léger, alors jouer un ''do'' et un ''sol'' simultanément n'est pas choquant. De même pour ''do'' et ''mi''. De là vient la notion de consonance.
Le statut du ''si''♭ est plus ambigu. Il fait partie des harmoniques qui sonnent naturellement, mais il forme une seconde descendante avec le ''do'', intervalle dissonant. Par ailleurs, on remarque que le ''si''♭ ne fait pas partie de la gamme de ''do'' majeur, contrairement au ''sol'' et au ''mi''.
Pour le jeu sur les dissonances, on peut écouter par exemple la ''Toccata'' en ''ré'' mineur, op. 11 de Sergueï Prokofiev (1912).
: {{lien web |url=https://www.youtube.com/watch?v=AVpnr8dI_50 |titre=Yuja Wang Prokofiev Toccata |site=YouTube |date=2019-02-26 |consulté le=2021-12-19}}
== Contrepoint ==
Dans le chant grégorien, la notion d'accord n'existe pas. L'harmonie provient de la superposition de plusieurs mélodies, notamment dans ce que l'on appelle le « contrepoint ».
Le terme provient du latin ''« punctum contra punctum »'', littéralement « point par point », et désigne le fait que les notes de chaque voix se correspondent.
L'exemple le plus connu de contrepoint est le canon, comme par exemple ''Frère Jacques'' : chaque note d'un couplet correspond à une note du couplet précédent.
Certains morceaux sont bâtis sur une écriture « en miroir » : l'ordre des notes est inversé entre les deux voix, ou bien les intervalles sont inversés (« mouvement contraire » : une tierce montante sur une voix correspond à une tierce descendante sur l'autre).
On peut également citer le « mouvement oblique » (une des voix, le bourdon, chante toujours la même note) et le mouvement parallèle (les deux voix chantent le même air mais transposé, l'une est plus aiguë que l'autre).
Nous reproduisons ci-dessous le début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.
[[Fichier:Haendel Sonate en trio re mineur debut canon.svg | vignette | center | upright=2 | Début du second ''Allergo'' de la sonate en trio en ''ré'' mineur de Haendel.]]
[[Fichier:Haendel Sonate en trio re mineur debut.midi | vignette | Début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.]]
Nous avons mis en évidence la construction en canon avec des encadrés de couleur : sur les quatre premières mesures, nous voyons trois thèmes repris alternativement par une voix et par l'autre. Ce type de procédé est très courant dans la musique baroque.
Les procédés du contrepoint s'appliquent également à la danse :
* unisson : les danseurs et danseuses font les mêmes gestes en même temps ;
* répétition : le fait de répéter une série de gestes, une « phrase dansante » ;
* canon : les gestes sont faits avec un décalage régulier d'un danseur ou d'une danseuse à l'autre ;
* cascade : forme de canon dans laquelle le décalage est très petit ;
* contraste : deux danseur·euses, ou deux groupes, ont des gestuelles très différentes ;
* accumulation : la gestuelle se complexifie par l'ajout d'éléments au fur et à mesure ; ou bien le nombre de danseur·euses augmente ;
* dialogue : les gestes de danseur·euses ou de groupes se répondent ;
* contre-point : la gestuelle d'un ou une danseuse se superpose à la gestuelle d'un groupe ;
* lâcher-rattraper : les danseurs et danseuses alternent danse à l'unisson et gestuelles indépendantes.
: {{lien web
| url=https://www.youtube.com/watch?v=wgblAOzedFc
| titre=Les procédés de composition en danse
| auteur= Doisneau Sport TV
| site=YouTube
| date=2020-03-16 | consulté le=2021-01-21
}}
{{...}}
== Les accords en général ==
Initialement, on a des chants polyphoniques, des voix qui chantent chacune une mélodie, les mélodies se mêlant. On remarque que certaines superpositions de notes sonnent de manière plus ou moins agréables, consonantes ou dissonantes. On en vient alors à associer ces notes, c'est-à-dire à considérer dès le départ la superposition de ces notes et non pas la rencontre de ces notes au gré des mélodies. Ces groupes de notes superposées forment les accords. En Europe, cette notion apparaît vers le {{pc|xiv}}<sup>e</sup> siècle avec notamment la ''[[wikipedia:fr:Messe de Notre Dame|Messe de Notre Dame]]'' de Guillaume de Machaut (vers 1360-1365). La notion « d'accord parfait » est consacrée par [[wikipedia:fr:Jean-Philippe Rameau|Jean-Philippe Rameau]] dans son ''Traité de l'harmonie réduite à ses principes naturels'', publié en 1722.
=== Qu'est-ce qu'un accord ? ===
Un accord est un ensemble d'au minimum trois notes jouées en même temps. « Jouées » signifie qu'il faut qu'à un moment donné, elles sonnent en même temps, mais le début ou la fin des notes peut être à des instants différents.
Considérons que l'on joue les notes ''do'', ''mi'' et ''sol'' en même temps. Cet accord s'appelle « accord de ''do'' majeur ». En musique classique, on lui adjoint l'adjectif « parfait » : « accord parfait de ''do'' majeur ».
Nous représentons ci-dessous trois manière de faire l'accord : avec trois instruments jouant chacun une note :
[[Fichier:Do majeur trois portees.svg|class=transparent|center|Accord de ''do'' majeur avec trois instruments différents.]]
Avec un seul instrument jouant simultanément les trois notes :
[[Fichier:Chord C.svg|class=transparent|center|Accord de ''do'' majeur joué par un seul instrument.]]
L'accord tel qu'il est joué habituellement par une guitare d'accompagnement :
[[Fichier:Do majeur guitare.svg|class=transparent|center|Accord de ''do'' majeur à la guitare.]]
Pour ce dernier, nous représentons le diagramme indiquant la position des doigts sur le manche au dessus de la portée et la tablature en dessous. Ici, c'est au total six notes qui sont jouées : ''mi'' grave, ''do'' médium, ''mi'' médium, ''sol'' médium, ''do'' aigu, ''mi'' aigu. Mais il s'agit bien des trois notes ''do'', ''mi'' et ''sol'' jouées à des octaves différentes. Nous remarquons également que la note de basse (la note la plus grave), ''mi'', est différente de la note fondamentale (celle qui donne le nom à l'accord), ''do'' ; l'accord est dit « renversé » (voir plus loin).
=== Comment joue-t-on un accord ? ===
Les notes ne sont pas forcément jouées en même temps ; elles peuvent être « égrainées », jouée successivement, ce que l'on appelle un arpège. La partition ci-dessous montre six manières différentes de jouer un accord de ''la'' mineur à la guitare, plaqué puis arpégé.
[[Fichier:La mineur differentes executions.svg|class=transparent|center|Différentes exécution de l'accord de do majeur à la guitare.]]
[[Fichier:La mineur differentes executions midi.midi|vignette|Différentes exécution de l'accord de la mineur à la guitare.]]
Vous pouvez écouter l'exécution de cette partition avec le lecteur ci-contre.
Seuls les instruments polyphoniques peuvent jouer les accords plaqués : instruments à clavier (clavecin, orgue, piano, accordéon), les instruments à plusieurs cordes pincées (harpe, guitare ; violon, alto, violoncelle et contrebasse joués en pizzicati). Les instruments à corde frottés de la famille du violon peuvent jouer des notes par deux à l'archet mais pas plus du fait de la forme bombée du chevalet ; cependant, un mouvement rapide permet de jouer les quatre cordes de manière très rapprochée. Les instruments à percussion de type xylophone ou le tympanon permettent de jouer jusqu'à quatre notes simultanément en tenant deux baguettes (mailloches, maillets) par main.
Tous les instruments peuvent jouer des arpèges même si, dans le cas des instruments monodiques, les notes ne continuent pas à sonner lorsque l'on passe à la note suivante.
L'arpège peut être joué par l'instrument de basse (basson, violoncelle, contrebasse, guitare basse, pédalier de l'orgue…), notamment dans le cas d'une basse continue ou d'une ''{{lang|en|walking bass}}'' (« basse marchante » : la basse joue des noires, donnant ainsi l'impression qu'elle marche).
En jazz, et spécifiquement au piano, on a recours au ''{{lang|en|voicing}}'' : on choisit la manière dont on organise les notes pour donner une couleur spécifique, ou bien pour créer une mélodie en enchaînant les accords. Il est fréquent de ne pas jouer toutes les notes : si on n'en garde que deux, ce sont la tierce et la septième, car ce sont celles qui caractérisent l'accord (selon que la tierce est mineure ou majeure, que la septième est majeure ou mineure), et la fondamentale est en général jouée par la contrebasse ou guitare basse.
{{clear}}
=== Classes d'accord ===
[[Fichier:Intervalles harmoniques accords classes.svg|vignette|upright=1.5|Intervalles harmoniques dans les accords classés de trois, quatre et cinq notes.]]
Un accord composé d'empilement de tierces est appelé « accord classé ». En musique tonale, c'est-à-dire la musique fondée sur les gammes majeures ou mineures (cas majoritaire en musique classique), on distingue trois classes d'accords :
* les accords de trois notes, ou triades, ou accords de quinte ;
* les accords de quatre notes, ou accords de septième ;
* les accords de cinq notes, ou accords de neuvième.
En empilant des tierces, si l'on part de la note fondamentale, on a donc de intervalles de tierce, quinte, septième et neuvième.
En musique tonale, les accords avec d'autres intervalles (hors renversement, voir ci-après), typiquement seconde, quarte ou sixte, sont considérés comme des transitions entre deux accords classés. Ils sont appelés, selon leur utilisation, « accords à retard » (en anglais : ''{{lang|en|suspended chord}}'', accord suspendu) ou « appoggiature » (note « appuyée », étrangère à l'harmonie). Voir aussi plus loin la notion de note étrangère.
=== Renversements d'accords ===
[[File:Accord do majeur renversements.svg|thumb|Accord parfait de do majeur et ses renversements.]]
[[Fichier:Progression dominante renverse parfait do majeur.svg|vignette|upright=0.6|Progression accord de dominante renversé → accord parfait en ''do'' majeur.]]
Un accord classé est donc un empilement de tierces. Si l'on change l'ordre des notes, on a toujours le même accord mais il est fait avec d'autres intervalles harmoniques. Par exemple, l'accord parfait de ''do'' majeur dans son état fondamental, c'est-à-dire non renversé, s'écrit ''do'' - ''mi'' - ''sol''. Sa note fondamentale, ''do'', est aussi se note de basse.
Si maintenant on prend le ''do'' de l'octave supérieure, l'accord devient ''mi - sol - do'' ; c'est l'empilement d'une tierce ''(mi - sol)'' et d'une quarte ''(sol - do)'', soit la superposition d'une tierce ''(mi - sol)'' et d'une sixième ''(mi - do)''. C'est le premier renversement de l'accord parfait de ''do'' majeur ; la fondamentale est toujours ''do'' mais la basse est ''mi''. Le second renversement est ''sol - do - mi''.
L'utilisation de renversement peut faciliter l'exécution de la progression d'accord. Par exemple, en tonalité ''do'' majeur, si l'on veut passer de l'accord de dominante ''sol - si - ré'' à l'accord parfait ''do - mi - sol'', alors on peut utiliser le second renversement de l'accord de dominante : ''ré - sol - si'' → ''do - mi - sol''. Ainsi, la basse descend juste d'un ton ''(ré → do)'' et sur un piano, la main reste globalement dans la même position.
Le renversement d'un accord permet également de respecter certaines règles de l'harmonie classique, notamment éviter que des voix se suivent strictement (« mouvement parallèle »), ce qui aurait un effet de platitude.
De manière générale, la notion de renversement permet deux choses :
* d'enrichir l'œuvre : pour créer une harmonie donnée (c'est-à-dire des sons sonnant bien ensemble), nous avons plus de souplesse, nous pouvons organiser ces notes comme nous le voulons selon les voix ;
* de simplifier l'analyse : quelle que soit la manière dont sont organisées les notes, cela nous ramène à un même accord.
{{citation bloc|Or il, y a plusieurs manières de jouer un accord, selon que l'on aborde par la première note qui le constitue, ''do mi sol'', la deuxième, ''mi sol do'', ou la troisième note, ''sol do mi''. Ce sont les renversements, [que Rameau] va classer en différentes combinaisons d'une seule matrice. Faisant cela, Rameau divise le nombre d'accords [de septième] par quatre. Il simplifie, il structure […].|{{ouvrage|prénom1=André |nom1=Manoukian |titre=Sur les routes de la musique |éditeur=Harper Collins |année=2021 |passage=54 |isbn=979-1-03391201-9}} }}
{{clear}}
[[File:Plusieurs realisation 1er renversement doM.svg|thumb|Plusieurs réalisation du premier renversement de l'accord de ''do'' majeur.]]
Notez que dans la notion de renversement, seule importe en fait la note de basse. Ainsi, les accords ''mi-sol-do'', ''mi-do-sol'', ''mi-do-mi-sol'', ''mi-sol-mi-do''… sont tous une déclinaison du premier renversement de ''do-mi-sol'' et ils seront abrégés de la même manière (''mi''<sup>6</sup> en musique classique ou C/E en musique populaire et jazz, voir plus bas).
{{clear}}
== Notation des accords de trois notes ==
Les accords de trois notes sont appelés « accords de quinte » en classique, et « triades » en jazz.
[[Fichier:Progression dominante renverse parfait do majeur chiffrage.svg|vignette|upright=0.7|Chiffrage du second renversement d'un accord de ''sol'' majeur et d'un accord de ''do'' majeur : notation en musique populaire et jazz (haut) et notation de basse chiffrée (bas).]]
Les accords sont construits de manière systématique. Nous pouvons donc les représenter de manière simplifiée. Cette notation simplifiée des accords est appelée « chiffrage ».
Reprenons la progression d'accords ci-dessus : « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » dans la tonalité de ''do'' majeur. On utilise en général trois notations différentes :
* en musique populaire, jazz, rock… un accord est désigné par sa note fondamentale ; ici donc, les accords sont notés « ''sol'' - ''do'' » ou, en notation anglo-saxonne, « G - C » ;<br /> comme le premier accord est renversé, on indique la note de basse après une barre, la progression d'accords est donc chiffrée '''« ''sol''/''ré'' - ''do'' »''' ou '''« G/D - C »''' ;<br /> il s'agit ici d'accords composés d'une tierce majeure et d'une quinte juste ; si les accords sont constitués d'intervalles différents, nous ajoutons un symbole après la note : « m » ou « – » si la tierce est mineure, « dim » ou « ° » si la quinte est diminuée ;
* en musique classique, on utilise la notation de « basse chiffrée » (utilisée notamment pour noter la basse continue en musique baroque) : on indique la note de basse sur la portée et on lui adjoint l'intervalle de la fondamentale à la note la plus haute (donc ici respectivement 6 et 5, puisque ''sol''-''si'' est une sixte et ''do''-''sol'' est une quinte), étant sous-entendu que l'on a des empilements de tierce en dessous ; mais dans le cas du premier accord, le premier intervalle n'est pas une tierce, mais une quarte ''(ré''-''sol)'', on note donc '''« ''ré'' <sup>6</sup><sub>4</sub> - ''do'' <sup>5</sup> »'''<ref>quand on ne dispose pas de la notation en supérieur (exposant) et inférieur (indice), on utilise parfois une notation sous forme de fraction : ''sol'' 6/4 et ''do'' 5/.</ref> ;
* lorsque l'on fait l'analyse d'un morceau, on s'attache à identifier la note fondamentale de l'accord (qui est différente de la basse dans le cas d'un renversement) ; on indique alors le degré de la fondamentale : '''« {{Times New Roman|V<sup>6</sup><sub>4</sub> - I<sup>5</sup>}} »'''.
La notation de basse chiffrée permet de construire l'accord à la volée :
* on joue la note indiquée (basse) ;
* s'il n'y a pas de 2 ni de 4, on lui ajoute la tierce ;
* on ajoute les intervalles indiqués par le chiffrage.
La notation de musique jazz oblige à connaître la composition des différents accords, mais une fois que ceux-ci sont acquis, il n'y a pas besoin de reconstruire l'accord.
La notation de basse chiffrée avec les chiffres romains n'est pas utilisée pour jouer, mais uniquement pour analyser ; Sur les partitions avec basse chiffrée, il y a simplement les chiffrages indiqués au-dessus de la partie de basse. Le chiffrage avec le degré en chiffres romains présente l'avantage d'être indépendant de la tonalité et donc de se concentrer sur la fonction de l'accord au sein de la tonalité. Par exemple, ci-dessous, nous pouvons parler de la progression d'accords « {{Times New Roman|V - I}} » de manière générale, cette notation étant valable quelle que soit la tonalité.
[[File:Progression dominante renverse parfait do majeur chiffrage basse continue.svg|thumb|Chiffrage en notation basse chiffrée de la progression d'accords « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » en do majeur.]]
{{note|En notation de base continue avec fondamentale en chiffres romains, la fondamentale est toujours indiquée ''sous'' la portée de la partie de basse. Les intervalles sont indiqués au-dessus de la portée de la partie de basse ; lorsque l'on fait une analyse, on peut ayssi les indiquer à côté du degré en chiffres romains, donc sous la portée de la basse.}}
{{note|En notation rock, le 5 en exposant indique un accord incomplet avec uniquement la fondamentale et la quinte, un accord sans tierce appelé « accord de puissance » ou ''{{lang|en|power chord}}''. Par exemple, C<sup>5</sup> est l'accord ''do-sol''.}}
{{clear}}
[[Fichier:Accords parfait do majeur basse chiffree fondamental et renverse.svg|vignette|upright=2.5|Chiffrage de l'accord parfait de ''do'' majeur en basse chiffrée, à l'état fondamental et ses renversements.]]
Concernant les accords parfaits en notation de basse chiffrée :
* un accord parfait à l'état fondamental est chiffré « <sup>5</sup> » ; on l'appelle « accord de quinte » ;
* le premier renversement est chiffré « <sup>6</sup> » (la tierce est implicite) ; on l'appelle « accord de sixte » ;
* le second renversement est noté « <sup>6</sup><sub>4</sub> » ; on l'appelle « accord de sixte et de quarte » (ou bien « de quarte et de sixte »).
Par exemple, pour l'accord parfait de ''do'' majeur :
* l'état fondamental ''do''-''mi''-''sol'' est noté ''do''<sup>5</sup> ;
* le premier renversement ''mi''-''sol''-''do'' est noté ''mi''<sup>6</sup> ;
* le second renversement ''sol''-''do''-''mi'' est noté ''sol''<sup>6</sup><sub>4</sub>.
Il y a une exception : l'accord construit sur la sensible (7{{e}} degré) contient une quinte diminuée et non une quinte juste. Le chiffrage est donc différent :
* l'état fondamental ''si''-''ré''-''fa'' est noté ''si''<sup><s>5</s></sup> (cinq barré), « accord de quinte diminuée » ;
* le premier renversement ''ré''-''fa''-''si'' est noté ''ré''<sup>+6</sup><sub>3</sub>, « accord de sixte sensible et tierce » ;
* le second renversement ''fa''-''si''-''ré'' est noté ''fa''<sup>6</sup><sub>+4</sub>, « accord de sixte et quarte sensible ».
Par ailleurs, on ne considère pas qu'il est fondé sur la sensible, mais sur la dominante ; on met donc des guillemets autour du degré, « “V” ». Donc selon l'état, le chiffrage est “V”<sup><s>5</s></sup>, “V”<sup>+6</sup><sub>3</sub> ou “V”<sup>6</sup><sub>+4</sub>.
En notation jazz, on ajoute « dim », « <sup>o</sup> » ou bien « <sup>♭5</sup> » au chiffrage, ici : B dim, B<sup>o</sup> ou B<sup>♭5</sup> pour l'état fondamental. Pour les renversements : B dim/D et B dim/F ; ou bien B<sup>o</sup>/D et B<sup>o</sup>/F ; ou bien B<sup>♭5</sup>/D et B<sup>♭5</sup>/F.
{{clear}}
[[Fichier:Accords basse chiffree basse do fondamental et renverses.svg|vignette|upright=2|Basse chiffrée : accords de quinte, de sixte et de sixte et de quarte ayant pour basse ''do''.]]
Et concernant les accords ayant pour basse ''do'' en tonalité de ''do'' majeur :
* l'accord ''do''<sup>5</sup> est un accord à l'état fondamental, c'est donc l'accord ''do''-''mi''-''sol'' (sa fondamentale est ''do'') ;
* l'accord ''do''<sup>6</sup> est le premier renversement d'un accord, c'est donc l'accord ''do''-''mi''-''la'' (sa fondamentale est ''la'') ;
* l'accord ''do''<sup>6</sup><sub>4</sub> est le second renversement d'un accord, c'est donc l'accord ''do''-''fa''-''la'' (sa fondamentale est ''fa'').
{{clear}}
== Notes étrangères ==
La musique européenne s'appuie essentiellement sur des accords parfaits, c'est-à-dire fondés sur une tierce majeure ou mineure, et une quinte juste. Il arrive fréquemment qu'un accord ne soit pas un accord parfait. Les notes qui font partie de l'accord parfait sont appelées « notes naturelles » et la note qui n'en fait pas partie est appelée « note étrangère ».
Il existe plusieurs types de notes étrangères :
* anticipation : la note étrangère est une note naturelle de l'accord suivant ;
* appogiature : note d'ornementation qui se résout par mouvement conjoint, c'est-à-dire qu'elle est suivie par une note située juste au-dessus ou en dessous (seconde ascendante ou descendante) qui est, elle, une note naturelle ;
* broderie : on part d'une note naturelle, on monte ou on descend d'une seconde, puis on revient sur la note naturelle ;
* double broderie : on part d'une note naturelle, on joue la note du dessus puis la note du dessous avant de revenir à la note naturelle ; ou bien on joue la note du dessous puis la note du dessus ;
* échappée : note étrangère n'appartenant à aucune des autres catégories ;
* note de passage : mouvement conjoint allant d'une note naturelle d'un accord à une note naturelle de l'accord suivant ;
* pédale : la note de basse reste la même pendant plusieurs accords successifs ;
* retard : la note étrangère est une note naturelle de l'accord précédent.
Les notes étrangères ne sont pas chiffrées.
[[File:Notes etrangeres accords.svg|center|Différents types de notes étrangères.]]
{{note|Les anglophones distinguent deux types de retard : la ''{{lang|en|suspension}}'' est résolue vers le haut (le mouvement est ascendant), le ''{{lang|en|retardation}}'' est résolu vers le bas (le mouvement est descendant).}}
== Principaux accords ==
Les trois principaux accords sont :
* l'accord parfait majeur : il est construit sur les degrés {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (médiante) et {{Times New Roman|V}} (dominante) d'une gamme majeure ; il est noté {{Times New Roman|I}}<sup>5</sup>, {{Times New Roman|IV}}<sup>5</sup>, {{Times New Roman|V}}<sup>5</sup> ;
* l'accord parfait mineur : il est construit sur les degrés {{Times New Roman|I}} (tonique) et {{Times New Roman|IV}} (sous-tonique) d'une gamme mineure harmonique ; il est également noté {{Times New Roman|I}}<sup>5</sup> et {{Times New Roman|IV}}<sup>5</sup>, les anglo-saxons le notent {{Times New Roman|i}}<sup>5</sup> et {{Times New Roman|iv}}<sup>5</sup> (la minuscule indiquant le caractère mineur) ;
* l'accord de septième de dominante : il est construit sur le degré {{Times New Roman|V}} (dominante) d'une gamme majeure ou mineure harmonique ; il est noté {{Times New Roman|V}}<sup>7</sup><sub>+</sub>.
On peut trouver ces trois accords sur d'autres degrés, et il existe d'autre types d'accords. Nous verrons cela plus loin.
{| class="wikitable"
|+ Constitution des principaux accords — Dénomination classique
|-
! scope="col" | Accord
! scope="col" | 1<sup>er</sup> intervalle
! scope="col" | 2<sup>e</sup> intervalle
! scope="col" | 3<sup>e</sup> intervalle
|-
! scope="row" | Accord parfait majeur
| tierce majeure (3M) || quinte juste (5J) || —
|-
! scope="row" | Accord parfait mineur
| tierce mineure (3m) || quinte juste (5J) || —
|-
! scope="row" | Accord de septième de dominante
| tierce majeure (3M) || quinte juste (5J) || septième mineure (7m)
|}
{| class="wikitable"
|+ Constitution des principaux accords — Dénomination jazz
|-
! scope="col" | Accord
! scope="col" | 1<sup>er</sup> intervalle
! scope="col" | 2<sup>e</sup> intervalle
! scope="col" | 3<sup>e</sup> intervalle
|-
! scope="row" | Triade majeure
| tierce majeure (3M) || quinte juste (5J) || —
|-
! scope="row" | Triade mineure
| tierce mineure (3m) || quinte juste (5J) || —
|-
! scope="row" | Accord de septième
| tierce majeure (3M) || quinte juste (5J) || septième mineure (7m)
|}
{| border="0"
|-
| [[Fichier:Accord do majeur arpege puis plaque.midi | Accord parfait de ''do'' majeur (C).]] || [[Fichier:Accord do mineur arpege puis plaque.midi | Accord parfait de ''do'' mineur (Cm).]] || [[Fichier:Accord do septieme arpege puis plaque.midi | Accord de septième de dominante de ''fa'' majeur (C<sup>7</sup>).]]
|-
| Accord parfait<br /> de ''do'' majeur (C). || Accord parfait<br /> de ''do'' mineur (Cm). || Accord de septième de dominante<br /> de ''fa'' majeur (C<sup>7</sup>).
|}
'''Rappel :'''
* la tierce mineure est composée d'un ton et demi (1 t ½) ;
* la tierce majeur est composée de deux tons (2 t) ;
* la quinte juste a la même altération que la fondamentale, sauf lorsque la fondamentale est ''si'' (la quinte juste est alors ''fa''♯) ;
* la septième mineure est le renversement de la seconde majeure (1 t).
[[File:Renversements accords pft fa maj basse chiffree.svg|thumb|Renversements de l'accord parfait de ''fa'' majeur, et la notation de basse chiffrée.]]
[[File:Renversements accord sept de dom fa maj basse chiffree.svg|thumb|Renversements de l'accord de septième de dominante de ''fa'' majeur, et la notation de basse chiffrée.]]
{| class="wikitable"
|+ Notation des principaux accords en musique classique
|-
! scope="col" | Accord
! scope="col" | État<br /> fondamental
! scope="col" | Premier<br /> renversement
! scope="col" | Deuxième<br /> renversement
! scope="col" | Troisième<br /> renversement
|-
! scope="row" | Accord parfait
| {{Times New Roman|I<sup>5</sup>}}<br/> acc. de quinte || {{Times New Roman|I<sup>6</sup>}}<br :> acc. de sixte || {{Times New Roman|I<sup>6</sup><sub>4</sub>}}<br /> acc. de quarte et de sixte || —
|-
! scope="row" | Accord de septième<br /> de dominante
| {{Times New Roman|V<sup>7</sup><sub>+</sub>}}<br /> acc.de septième de dominante || {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}<br />acc. de sixte et quinte diminuée || {{Times New Roman|V<sup>+6</sup>}}<br />acc. de sixte sensible || {{Times New Roman|V<sup>+4</sup>}}<br />acc. de quarte sensible<br />acc. de triton
|}
{| class="wikitable"
|+ Notation des principaux accords en jazz
|-
! scope="col" | Accord
! scope="col" | Tierce
! scope="col" | Quinte
! scope="col" | Septième
! scope="col" | Chiffrage
|-
! scope="row" | Triade majeure
| 3M || 5J || || X
|-
! scope="row" | Triade mineure
| 3m || 5J || || Xm, X–
|-
! scope="row" | Triade diminuée
| 3m || 5d || || X<sup>o</sup>, X<sub>m</sub><sup>(♭5)</sup>
|-
! scope="row" | Triade augmentée
| 3M || 5A || || X<sup>+</sup>, X<sup>(♯5)</sup>
|-
! scope="row" | Septième
| 3M || 5J || 7m || X<sup>7</sup>
|}
En jazz, les renversements se notent en mettant la basse après une barre de fraction, par exemple pour la triade de ''do'' majeur :
* état fondamental : C ;
* premier renversement : C/E ;
* second renversement : C/G.
{{clear}}
Dans le cas d'un accord de septième de dominante, le nom de l'accord change selon que l'on est en musique classique ou en jazz : en musique classique, on donne le nom de la tonalité alors qu'en jazz, on donne le nom de la fondamentale. Ainsi, l'accord appelé « septième de dominante de ''do'' majeur » en musique classique, est appelé « ''sol'' sept » (G<sup>7</sup>) en jazz : la dominante (degré {{Times New Roman|V}}, dominante) de la tonalité de ''do'' majeur est la note ''sol''.
Comment appelle-t-on en musique classique l'accord appelé « ''do'' sept » (C<sup>7</sup>) en jazz ? Les tonalités dont le ''do'' est la dominante sont les tonalités de ''fa'' majeur (''si''♭ à la clef) et de ''fa'' mineur harmonique (''si''♭, ''mi''♭, ''la''♭ et ''ré''♭ à la clef et ''mi''♮ accidentel). Il s'agit donc de l'accord de septième de dominante des tonalités de ''fa'' majeur et ''fa'' mineur harmonique.
{| class="wikitable"
|+ Accords fréquents pour quelques la tonalités majeures
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Accord parfait<br />{{Times New Roman|I<sup>5</sup>}}
! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}}
|-
|''Do'' majeur || || C<br />''do-mi-sol'' || G7<br />''sol-si-ré-fa''
|-
|''Sol'' majeur || ''fa''♯ || G<br />''sol-si-ré'' || D7<br />''ré-fa''♯''-la-do''
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || D<br />''ré-fa''♯''-la'' || A7<br />''la-do''♯''-mi-sol''
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A<br />''la-do''♯''-mi'' || E7<br />''mi-sol''♯''-si-ré''
|-
| ''Fa'' majeur || ''si''♭ || F<br />''fa-la-do'' || C7<br />''do-mi-sol-si''♭
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭<br />''si''♭''-ré-fa'' || F7<br />''fa-la-do-mi''♭
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭<br />''mi''♭''-sol-si''♭ || B♭7<br />''si''♭''-ré-fa-la''♭
|}
{| class="wikitable"
|+ Accords fréquents pour quelques la tonalités mineures harmoniques
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Accord parfait<br />{{Times New Roman|i<sup>5</sup>}}
! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}}
|-
|''La'' mineur<br />harmonique || || Am, A–<br />''la-do-mi'' || E7<br />''mi-sol''♯''-si-ré''
|-
|''Mi'' mineur<br />harmonique || ''fa''♯ || Em, E–<br />''mi-sol-si'' || B7<br />''si-ré''♯''-fa''♯''-la''
|-
|''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || Bm, B–<br />''si-ré-fa''♯ || F♯7<br />''fa''♯''la''♯''-do''♯''-mi''
|-
|''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || F♯m, F♯–<br />''fa''♯''-la-do''♯ || C♯7<br />''do''♯''-mi''♯''-sol''♯''-si''
|-
| ''Ré'' mineur<br />harmonique || ''si''♭ || Dm, D–<br />''ré-fa-la'' || A7<br />''la-do''♯''-mi-sol''
|-
| ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || Gm, G–<br />''sol-si''♭''-ré'' || D7<br />''ré-fa''♯''-la-do''
|-
| ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || Cm, C–<br />''do-mi''♭''-sol'' || G7<br />''sol-si''♮''-ré-fa''
|}
{{clear}}
== Accords sur les degrés d'une gamme ==
=== Harmonisation d'une gamme ===
[[Fichier:Accord trois notes gamme do majeur chiffre.svg|vignette|upright=1.2|Accords de trois note sur la gamme de ''do'' majeur, chiffrés.]]
On peut ainsi construire une triade par degré d'une gamme.
Pour une gamme majeure, les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|V<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|II<sup>5</sup>}}, {{Times New Roman|III<sup>5</sup>}}, {{Times New Roman|VI<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|ii<sup>5</sup>}}, {{Times New Roman|iii<sup>5</sup>}}, {{Times New Roman|vi<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}.
Les accords ont tous une quinte juste à l'exception de l'accord {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} qui a une quinte diminuée, raison pour laquelle le « 5 » est barré. C'est un accord dit « de quinte diminuée ». En jazz, l'accord diminué est noté « dim », « ° », « m<sup>♭5</sup> » ou « <sup>–♭5</sup> ».
Nous avons donc trois types d'accords (dans la notation jazz) : X (triade majeure), Xm (triade mineure) et X° (triade diminuée), la lettre X remplaçant le nom de la note fondamentale.
{{clear}}
[[Fichier:Accord trois notes gamme la mineur chiffre.svg|vignette|upright=1.2|Accords de trois notes sur une gamme de ''la'' mineur harmonique, chiffrés.]]
Pour une gamme mineure harmonique, les accords {{Times New Roman|III<sup>+5</sup>}}, {{Times New Roman|V<sup>♯</sup>}} et {{Times New Roman|VI<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|II<sup><s>5</s></sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|i<sup>5</sup>}}, {{Times New Roman|ii<sup><s>5</s></sup>}}, {{Times New Roman|iv<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}.
Les accords {{Times New Roman|ii<sup><s>5</s></sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} ont une quinte diminuée ; ce sont des accords dits « de quinte diminuée ». L'accord {{Times New Roman|III<sup>+5</sup>}} a une quinte augmentée ; le signe « plus » indique que la note de cinquième, le ''sol'' dièse, est la sensible. En jazz, l'accord est noté « aug » ou « <sup>+</sup> ». Les autres accords ont une quinte juste.
Aux trois accords générés par une gamme majeure (X, Xm et X°), nous voyons ici apparaître un quatrième type d'accord : la triade augmentée X<sup>+</sup>.
Nous remarquons que des gammes ont des accords communs. Par exemple, l'accord {{Times New Roman|ii<sup>5</sup>}} de ''do'' majeur est identique à l'accord {{Times New Roman|iv<sup>5</sup>}} de ''la'' mineur (il s'agit de l'accord Dm).
Quel que soit le mode, les accords construits sur la sensible (accord de quinte diminuée) sont rarement utilisés. S'ils le sont, c'est en tant qu'accord de septième de dominante sans fondamentale (voir ci-après). C'est la raison pour laquelle le chiffrage indique le degré {{Times New Roman|V}} entre guillemets, et non pas le degré {{Times New Roman|VII}} (mais pour des raisons de clarté, nous l'indiquons entre parenthèses au début).
En mode mineur, l'accord de quinte augmentée {{Times New Roman|iii<sup>+5</sup>}} est très peu utilisé (voir plus loin ''[[#Progression_d'accords|Progression d'accords]]''). C'est un accord considéré comme dissonant.
On voit que :
* un accord parfait majeur peut appartenir à cinq gammes différentes ;<br /> par exemple l'accord parfait de ''do'' majeur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> degré de la gamme de ''do'' majeur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' mineur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''mi'' mineur ;
* un accord parfait mineur peut appartenir à cinq gammes différentes ;<br />par exemple l'accord parfait de ''la'' mineur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> de la gamme de ''la'' mineur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''mi'' mineur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|III}}<sup>e</sup> degré de ''fa'' majeur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''do'' majeur ;
* un accord de quinte diminuée peut appartenir à trois gammes différentes ;<br />par exemple, l'accord de quinte diminuée de ''si'' est l'accord construit sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' majeur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''la'' mineur et sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' mineur ;
* un accord de quinte augmentée (à l'état fondamental) ne peut appartenir qu'à une seule gamme ;<br /> par exemple, l'accord de quinte augmentée de ''do'' est l'accord construit sur le {{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur.
=== Harmonisation par des accords de septième ===
[[Fichier:Harmonisation gamme do majeur par septiemes chiffre.svg|vignette|upright=2|Harmonisation de la gamme de do majeur par des accords de septième.]]
Les accords de septième contiennent une dissonance et créent ainsi une tension. Ils sont très utilisés en jazz. Nous avons représenté ci-contre l'harmonisation de la gamme de ''do'' majeur.
La constitution des accords est la suivantes :
* tierce majeure (3M)
** quinte juste (5J)
*** septième mineure (7m) : sur le degré V, c'est l'accord de septième de dominante V<sup>7</sup><sub>+</sub>, noté X<sup>7</sup> (X pour G),
*** septième majeure (7M) : sur les degrés I et IV, appelés « accords de septième majeure » et notés aussi X<sup>maj7</sup> ou X<sup>Δ</sup> (X pour C ou F) ;
* tierce mineure (3m)
** quinte juste (5J)
*** septième mineure : sur les degrés ii, iii et vi, appelés « accords mineur septième » et notés Xm<sup>7</sup> ou X–<sup>7</sup> (X pour D, E ou A),
** quinte diminuée (5d)
*** septième mineure (7m) : sur le degré vii, appelé « accord demi-diminué » (puisque seule la quinte est diminuée) et noté X<sup>∅</sup> ou Xm<sup>7(♭5)</sup> ou X–<sup>7(♭5)</sup> (X pour B) ;<br /> en musique classique, on considère que c'est un accord de neuvième de dominante sans fondamentale.
Nous avons donc quatre types d'accords : X<sup>7</sup>, X<sup>maj7</sup>, Xm<sup>7</sup> et X<sup>∅</sup>
En jazz, on ajoute souvent la quarte à l'accord de sous-dominante IV (sur le ''fa'' dans une gamme de ''do'' majeur) ; il s'agit ici d'une quarte augmentée (''fa''-''si'') et l'accord est surnommé « accord lydien » mais cette dénomination est erronée (il s'agit d'une mauvaise interprétation de textes antiques). C'est un accord de onzième sans neuvième (la onzième étant l'octave de la quarte), il est noté X<sup>maj7(♯11)</sup> ou X<sup>Δ(♯11)</sup> (ici, F<sup>maj7(♯11)</sup>, ''fa''-''la''-''do''-''mi''-''si'' ou ''fa''-''la''-''si''-''do''-''mi'').
=== Modulation et emprunt ===
Un morceau peut comporter des changements de tonalité; appelés « modulation ». Il y a parfois un court passage dans une tonalité différente, typiquement sur une ou deux mesures, avant de retourner dans la tonalité d'origine : on parle d'emprunt. Lorsqu'il y a une modulation ou un emprunt, les degrés changent. Un même accord peut donc avoir une fonction dans une partie du morceau et une autre fonction ailleurs. L'utilisation d'accord différents, et en particulier d'accord utilisant des altérations accidentelles, indique clairement une modulation.
Nous avons vu précédemment que les modulations courantes sont :
* les modulations dans les tons voisins ;
* les modulations homonymes ;
* les marches harmoniques.
Une modulation entre une tonalité majeure et mineure change la couleur du passage,
* la modulation la plus « douce » est entre les tonalités relatives (par exemple''do'' majeur et ''la'' mineur) car ces tonalités utilisent quasiment les mêmes notes ;
* la modulation la plus « voyante » est la modulation homonyme (par exemple entre ''do'' majeur et ''do'' mineur).
Une modulation commence souvent sur l'accord de dominante de la nouvelle tonalité.
Pour analyser un œuvre, ou pour improviser sur une partie, il est important de reconnaître les modulations. La description de la successind es tonalités s'appelle le « parcours tonal ».
=== Exercices élémentaires ===
L'apprentissage des accords passe par quelques exercices élémentaires.
'''1. Lire un accord'''
Il s'agit de lecture de notes : des notes composant les accords sont écrites « empilées » sur une portée, il faut les lire en énonçant les notes de bas en haut.
'''2. Reconnaître la « couleur » d'un accord'''
On écoute une triade et il faut dire si c'est une triade majeure ou mineure. Puis, on complexifie l'exercice en ajoutant la septième.
'''3. Chiffrage un accord'''
Trouver le nom d'un accord à partir des notes qui le composent.
'''4. Réalisation d'un accord'''
Trouver les notes qui composent un accord à partir de son nom.
'''5. Dictée d'accords'''
On écoute une succession d'accords et il faut soit écrire les notes sur une portée, soit écrire les noms de accords.
[[File:Exercice constitution accord basse chiffree.svg|thumb|Exercice : constitution d'accord à partir de la basse chiffrée.]]
'''Exercices de basse chiffrée'''
''Réalisation d'un accord''
Sur la figure suivante, écrire les notes des accords correspondant à la basse chiffrée. Déterminer le degré de la fondamentale pour chaque accord en considérant que nous sommes dans la tonalité de ''sol'' majeur.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice constitution accord basse chiffree solution.svg|vignette|Solution.]]
# La note de basse est un ''do''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''mi'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''sol''.<br />Le chiffrage « <sup>5</sup> » indique que c'est un accord dans son état fondamental (l'écart entre deux notes consécutives ne dépasse pas la tierce), la fondamentale est donc la basse, ''do'', qui est le degré IV de la tonalité.
# La note de basse est un ''si''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''ré'', puis nous appliquons le chiffrage 6 et ajoutons la sixte, ''sol''.<br />Le chiffrage « <sup>6</sup> » indique que c'est un accord dans son premier renversement. En le remettant dans son état fondamental, nous obtenons ''sol-si-ré'', la fondamentale est donc la tonique, le degré I.
# La note de basse est un ''la''. Nous ajoutons la tierce (chiffre 3), ''do'', et la sixte (6), ''fa''♯. Nous vérifions que le ''fa''♯ est la sensible (signe +)<br />Nous voyons un « blanc » entre les notes ''do'' et ''fa''♯. En descendant le ''fa''♯ à l'octave inférieure, nous obtenons un empilement de tierces ''fa''♯-''la-do'', le fondamentale est donc ''fa''♯, le degré VII. Nous pouvons le voir comme le deuxième renversement de l'accord de septième de dominante, sans fondamentale.
# La note de basse est un ''fa''♯. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''la'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''do'' ; nous vérifions qu'il s'agit bien d'une quinte diminuée (le 5 est barré). Nous appliquons le chiffre 6 et ajoutons la sixte, ''ré''.<br />Nous voyons que les notes ''do'' et ''ré'' sont conjointes (intervalle de seconde). En descendant le ''ré'' à l'octave inférieure, nous obtenons un empilement de tierces ''ré-fa''♯-''la-do'', le fondamentale est donc ''ré'', le degré V. Nous constatons que l'accord chiffré est le premier renversement de l'accord de septième de dominante.
{{boîte déroulante/fin}}
{{clear}}
''Chiffrage d'accords''
[[Fichier:Exercice chiffrage accord basse chiffree.svg|vignette|Accords à chiffrer.]]
Chiffrer les accords ci-contre.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice chiffrage accord basse chiffree solution.svg|vignette|Solution.]]
# On relève les intervalles en partant de la basse : tierce majeure (3M) et quinte juste (5J). Le chiffrage complet est donc ''fa''<sup>5</sup><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''fa''<sup>5</sup>.<br /> On peut aussi reconnaître que c'est l'accord parfait sur la tonique de la tonalité de ''fa'' majeur dans son état fondamental, le chiffrage d'un accord parfait étant <sup>5</sup>.
# On relève les intervalles en partant de la basse : quarte juste (4J), sixte majeure (6M). Le chiffrage complet est donc ''fa''<sup>6</sup><sub>4</sub>.<br /> On peut aussi reconnaître que c'est le second renversement de l'accord ''mi-sol-si'', sur la tonique de la tonalité de ''mi'' mineur, le chiffrage du second renversement d'un accord parfait étant <sup>6</sup><sub>4</sub>.
# Les intervalles en partant de la basse sont : tierce mineure (3m), quinte diminuée (5d), sixte mineure (6m). Le chiffrage complet est donc ''mi''<sup>6</sup><small><s>5</s></small><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''mi''<sup>6</sup><sub><s>5</s></sub>.<br /> On reconnaît le premier renversement de l'accord ''do-mi-sol-si''♭, accord de septième de dominante de la tonalité de ''fa'' majeur.
# Les intervalles en partant de la basse sont : tierce mineure (3m), quinte juste (5J), septième mineure (7m). Le chiffrage complet est donc ''ré''<sup>7</sup><small>5</small><sub>3</sub> ; c'est typique d'un accord de septième de dominante, son chiffrage est donc ''ré''<sup>7</sup><sub>+</sub>.<br /> On reconnaît l'accord de septième de dominante de la tonalité de ''sol'' mineur dans son état fondamental.
{{boîte déroulante/fin}}
{{clear}}
[[File:Exercice constitution accord notation jazz.svg|thumb|Exercice : constitution d'un accord d'après son chiffrage en notation jazz.]]
'''Exercices de notation jazz'''
''Réalisation d'un accord''
Sur la figure suivante, écrire les notes des accords correspondant aux chiffrages.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice constitution accord notation jazz solution.svg|thumb|Solution.]]
# Il s'agit de la triade majeure de ''do'' dans son état fondamental. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''do-mi-sol''.
# Il s'agit de la triade majeure de ''sol''. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''sol-si-ré''. On renverse l'accord afin que la basse soit le ''si'', l'accord est donc ''si-ré-sol''.
# Il s'agit de l'accord demi-diminué de ''fa''♯. Les intervalles sont la tierce mineure (3m), la quinte diminuée (5d) et la septième mineure (7m). Les notes sont donc ''fa''♯-''la-do-mi''. Nous renversons l'accord afin que la basse soit le ''la'', l'accord est donc ''a-do-mi-fa''♯.
# Il s'agit de l'accord de septième de ''ré''. Les intervalles sont donc la tierce majeure (3M), la quinte juste (5J) et la septième mineure (7m). Les notes sont ''ré-fa''♯''-la-do''. Nous renversons l'accord afin que la basse soit le ''fa''♯, l'accord est donc ''fa''♯''-la-do-ré''.
{{boîte déroulante/fin}}
{{clear}}
''Chiffrage d'accords''
[[File:Exercice chiffrage accord notation jazz.svg|thumb|Accords à chiffrer.]]
Chiffrer les accords ci-contre.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice chiffrage accord notation jazz solution.svg|thumb|Solution.]]
# Les notes sont toutes sur des interlignes consécutifs, c'est donc un empilement de tierces ; l'accord est dans son état fondamental. Les intervalles sont une tierce majeure (''fa-la'' : 3M) et une quinte juste (''fa-do'' : 5J), c'est donc la triade majeure de ''fa''. Le chiffrage est F.
# Il y a un blanc dans l'empilement des notes, c'est donc un accord renversé. En permutant les notes pour n'avoir que des tierces, on trouve l'accord ''mi-sol-si''. Les intervalles sont une tierce mineure (''mi-sol'' : 3m) et une quinte juste (''mi-si'' : 5J), c'est donc la triade mineure de ''mi'' avec un ''si'' à la basse. Le chiffrage est Em/B ou E–/B.
# Il y deux notes conjointes, c'est donc un renversement. L'état fondamental de cet accord est ''do-mi-sol-si''♭. Les intervalles sont une tierce majeure (''do-mi'' : 3M), une quinte juste (''do-sol'' : 5J) et une septième mineure (''do-si''♭ : 7m). C'est donc l'accord de ''do'' septième avec un ''mi'' à la basse, chiffré C<sup>7</sup>/E.
# Les notes sont toutes sur des interlignes consécutifs, l'accord est dans son état fondamental. Les intervalles sont la tierce mineure (''ré-fa'' : 3m), une quinte juste (''ré-la'' : 5J) et une septième mineure (''ré-do'' : 7m). C'est donc l'accord de ''ré'' mineur septième, chiffré Dm<sup>7</sup> ou D–<sup>7</sup>.
{{boîte déroulante/fin}}
{{clear}}
== Harmonie fonctionnelle ==
Le choix des accords et de leur succession — la progression des accords — est un élément important d'un morceau, de sa composition. Le compositeur ou la compositrice a bien sûr une liberté totale, mais pour faire des choix, il faut comprendre les conséquences de ces choix, et donc ici, les effets produits par les accords et leur progression.
Une des manières d'aborder le sujet est l'harmonie fonctionnelle.
=== Les trois fonctions des accords ===
En harmonie tonale, on considère que les accords ont une fonction. Il existe trois fonctions :
* la fonction de tonique, {{Times New Roman|I}} ;
* la fonction de sous-dominante, {{Times New Roman|IV}} ;
* la fonction de dominante, {{Times New Roman|V}}.
L'accord de tonique, {{Times New Roman|I}}, est l'accord « stable » de la tonalité par excellence. Il conclut en général les morceaux, et ouvre souvent les morceaux ; il revient fréquemment au cours du morceau.
L'accord de dominante, {{Times New Roman|V}}, est un accord qui introduit une instabilité, une tension. En particulier, il contient la sensible (degré {{Times New Roman|VI}}), qui est une note « aspirée » vers la tonique. Cette tension, qui peut être renforcée par l'utilisation d'un accord de septième, est fréquemment résolue par un passage vers l'accord de tonique. Nous avons donc deux mouvements typiques : {{Times New Roman|I}} → {{Times New Roman|V}} (création d'une tension, d'une attente) et {{Times New Roman|V}} → {{Times New Roman|I}} (résolution d'une tension). Les accords de tonique et de dominante ont le cinquième degré en commun, cette note sert donc de pivot entre les deux accords.
L'accord de sous-dominante, {{Times New Roman|IV}}, est un accord qui introduit lui aussi une tension, mais moins grande : il ne contient pas la sensible. Notons que s'il est une quarte au-dessus de la tonique, il est aussi une quinte en dessous d'elle ; il est symétrique de l'accord de dominante. Il a donc un rôle similaire à l'accord de dominante, mais atténué. L'accord de sous-dominante aspire soit vers l'accord de dominante, très proche, et l'on a alors une augmentation de la tension ; soit vers l'accord de tonique, un retour vers la stabilité (il a alors un rôle semblable à la dominante). Du fait de ces deux bifurcations possibles — augmentation de la tension ({{Times New Roman|IV}} → {{Times New Roman|V}}) ou retour à la stabilité ({{Times New Roman|IV}} → {{Times New Roman|I}}) —, l'utilisation de l'accord de sous-dominante introduit un certain flottement : si l'on peut facilement prédire l'accord qui suit un accord de dominante, on ne peut pas prédire ce qui suit un accord de sous-dominante.
Notons que la composition ne consiste pas à suivre ces règles de manière stricte, ce qui conduirait à des morceaux stéréotypés et plats. Le plaisir d'écoute joue sur une alternance entre satisfaction d'une attente (respect des règles) et surprise (rompre les règles).
=== Accords remplissant ces fonctions ===
Les accords sur les autres degrés peuvent se ramener à une de ces trois fonctions :
* {{Times New Roman|II}} : fonction de sous-dominante {{Times New Roman|IV}} ;
* {{Times New Roman|III}} (très peu utilisé en mode mineur en raison de sa dissonance) et {{Times New Roman|VI}} : fonction de tonique {{Times New Roman|I}} ;
* {{Times New Roman|VII}} : fonction de dominante {{Times New Roman|V}}.
En effet, les accords étant des empilements de tierces, des accords situés à une tierce l'un de l'autre — {{Times New Roman|I}} ↔ {{Times New Roman|III}}, {{Times New Roman|II}} ↔ {{Times New Roman|IV}}, {{Times New Roman|V}} ↔ {{Times New Roman|VII}}, {{Times New Roman|VI}} ↔ {{Times New Roman|VIII}} ( = {{Times New Roman|I}}) — ont deux notes en commun. On retrouve le fait que l'accord sur le degré {{Times New Roman|VII}} est considéré comme un accord de dominante sans tonique. En mode mineur, l'accord sur le degré {{Times New Roman|III}} est évité, il n'a donc pas de fonction.
{|class="wikitable"
|+ Fonction des accords
|-
! scope="col" | Fondamentale
! scope="col" | Fonction
|-
| {{Times New Roman|I}} || tonique
|-
| {{Times New Roman|II}} || sous-dominante faible
|-
| {{Times New Roman|III}} || tonique faible
|-
| {{Times New Roman|IV}} || sous-dominante
|-
| {{Times New Roman|V}} || dominante
|-
| {{Times New Roman|VI}} || tonique faible
|-
| {{Times New Roman|VII}} || dominante faible
|}
Par exemple en ''do'' majeur :
* fonction de tonique : '''''do''<sup>5</sup> (C)''', ''mi''<sup>5</sup> (E–), ''la''<sup>5</sup> (A–) ;
* fonction de sous-dominante : '''''fa''<sup>5</sup> (F)''', ''ré''<sup>5</sup> (D–) ;
* fonction de dominante : '''''sol''<sup>5</sup> (G)''' ou ''sol''<sup>7</sup><sub>+</sub> (G<sup>7</sup>), ''si''<sup> <s>5</s></sup> (B<sup>o</sup>).
En ''la'' mineur harmonique :
* fonction de tonique : '''''la''<sup>5</sup> (A–)''', ''fa''<sup>5</sup> (F) [, rarement : ''do''<sup>+5</sup> (C<sup>+</sup>)] ;
* fonction de sous-dominante : '''''ré''<sup>5</sup> (D–)''', ''si''<sup> <s>5</s></sup> (B<sup>o</sup>) ;
* fonction de dominante : '''''mi''<sup>5</sup> (E)''' ou ''mi''<sup>7</sup><sub>+</sub> (E<sup>7</sup>), ''sol''♯<sup> <s>5</s></sup> (G♯<sup>o</sup>).
Le fait d'utiliser des accords différents pour remplir une fonction permet d'enrichir l'harmonie, et de jouer sur l'équilibre entre satisfaction d'une attente (on respecte les règles sur les fonctions) et surprise (mais on n'utilise pas l'accord attendu).
=== Les dominantes secondaires ===
On utilise aussi des accords de septième dominante se fondant sur un autre degré que la dominante de la gamme ; on parle de « dominante secondaire ». Typiquement, avant un accord de septième de dominante, on utilise parfois un accord de dominante de dominante, dont le degré est alors noté « {{Times New Roman|V}} de {{Times New Roman|V}} » ou « {{Times New Roman|V}}/{{Times New Roman|V}} » ; la fondamentale est de l'accord est alors situé cinq degrés au-dessus de la dominante ({{Times New Roman|V}}), c'est donc le degré {{Times New Roman|IX}}, c'est-à-dire le degré {{Times New Roman|II}} de la tonalité en cours). Ou encore, on utilise un accord de dominante du degré {{Times New Roman|IV}} (« {{Times New Roman|V}} de {{Times New Roman|IV}} », la fondamentale est alors le degré {{Times New Roman|I}}) avant un accord sur le degré {{Times New Roman|IV}} lui-même.
Par exemple, en tonalité de ''do'' majeur, on peut trouver un accord ''ré - fa''♯'' - la - do'' (chiffré {{Times New Roman|V}} de {{Times New Roman|V}}<sup>7</sup><sub>+</sub>), avant un accord ''sol - si - ré - fa'' ({{Times New Roman|V}}<sup>7</sup><sub>+</sub>). L'accord ''ré - fa''♯'' - la - do'' est l'accord de septième de dominante des tonalités de ''sol''. Dans la même tonalité, on pourra utiliser un accord ''do - mi - sol - si''♭ ({{Times New Roman|V}} de {{Times New Roman|IV}}<sup>7</sup><sub>+</sub>) avant un accord ''fa - la - do'' ({{Times New Roman|IV}}<sup>5</sup>). Le recours à une dominante secondaire peut atténuer une transition, par exemple avec un enchaînement ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> → ''fa''<sup>5</sup> (C → C<sup>7</sup> → F) qui correspond à un enchaînement {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}} : le passage ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> (C → C<sup>7</sup>) se fait en ajoutant une note (le ''si''♭) et rend naturel le passage ''do'' → ''fa''.
Sur les sept degré de la gamme, on ne considère en général que cinq dominantes secondaires : en effet, la dominante du degré {{Times New Roman|I}} est la dominante « naturelle, primaire » de la tonalité (et n'est donc pas secondaire) ; et utiliser la dominante de {{Times New Roman|VII}} consisterait à considérer l'accord de {{Times New Roman|VII}} comme un accord propre, on évite donc les « {{Times New Roman|V}} de “{{Times New Roman|V}}” » (mais les « “{{Times New Roman|V}}” de {{Times New Roman|V}} » sont tout à fait « acceptables »).
=== Enchaînements classiques ===
Nous avons donc vu que l'on trouve fréquemment les enchaînements suivants :
* pour créer une instabilité :
** {{Times New Roman|I}} → {{Times New Roman|V}},
** {{Times New Roman|I}} → {{Times New Roman|IV}} (instabilité moins forte mais incertitude sur le sens d'évolution) ;
* pour maintenir l'instabilité :
** {{Times New Roman|IV}} → {{Times New Roman|V}} ;
* pour résoudre l'instabilité :
** {{Times New Roman|IV}} → {{Times New Roman|I}},
** {{Times New Roman|V}} → {{Times New Roman|I}}, cas particuliers (voir plus bas) :
*** {{Times New Roman|V}}<sup>+4</sup> → {{Times New Roman|I}}<sup>6</sup>,
*** {{Times New Roman|I}}<sup>6</sup><sub>4</sub> → {{Times New Roman|V}}<sup>7</sup><sub>+</sub> → {{Times New Roman|I}}<sup>5</sup>.
Les degrés indiqués ci-dessus sont les fonctions ; on peut donc utiliser les substitutions suivantes :
* {{Times New Roman|I}} par {{Times New Roman|VI}} et, en tonalité majeure, {{Times New Roman|III}} ;
* {{Times New Roman|IV}} par {{Times New Roman|II}} ;
* {{Times New Roman|V}} par {{Times New Roman|VII}}.
Pour enrichir l'harmonie, on peut utiliser les dominantes secondaires, en particulier :
* {{Times New Roman|V}} de {{Times New Roman|V}} ({{Times New Roman|II}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|V}},
* {{Times New Roman|V}} de {{Times New Roman|IV}} ({{Times New Roman|I}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|IV}}.
On peut enchaîner les enchaînements, par exemple {{Times New Roman|I}} → {{Times New Roman|IV}} → {{Times New Roman|V}}, ou encore {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}}… En jazz, on utilise très fréquemment l'enchaînement {{Times New Roman|II}} → {{Times New Roman|V}} → {{Times New Roman|I}} (deux-cinq-un).
On peut bien sûr avoir d'autres enchaînements, mais ces règles permettent d'analyser un grand nombre de morceaux, et donnent des clefs utiles pour la composition. Nous voyons ci-après un certain nombre d'enchaînements courants dans différents styles
== Exercice ==
Un hautboïste travaille la sonate en ''do'' mineur S. 277 de Heinichen. Sur le deuxième mouvement ''Allegro'', il a du mal à travailler un passage en raison des altérations accidentelles. Sur la suggestion de sa professeure, il décide d'analyser la progression d'accords sous-jacente afin que les altérations deviennent logiques. Il s'agit d'un duo hautbois et basson pour lequel les accords ne sont pas chiffrés, le basson étant ici un instrument soliste et non pas un élément de la basse continue.
Sur l'extrait suivant, déterminez les basses et la qualité (chiffrage) des accords sous-jacents. Commentez.
[[Fichier:Sonate hautbois basson heinichen 2e mvt mes49.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen.]]
{{note|L'œuvre est en ''do'' mineur et devrait donc avoir trois bémols à la clef, or ici il n'y en a que deux. En effet, le ''la'' pouvant être bécarre en mode mineur mélodique ascendant, le compositeur a préféré le noter explicitement en altération accidentelle lorsque l'on est en mode mélodique naturel, harmonique ou mélodique descendant. C'est un procédé assez courant à l'époque baroque.}}
{{boîte déroulante/début|titre=Solution}}
Une des difficultés ici est que les arpèges joués par les instruments sont agrémentés de notes de passage.
Les notes de la basse (du basson) sont différentes entre le premier et le deuxième temps de chaque mesure et ne peuvent pas appartenir au même accord. On a donc un accord par temps.
Sur le premier temps de chaque mesure, le basson joue une octave. La note concernée est donc la basse de chaque accord. Pour savoir s'il s'agit d'un accord à l'état fondamental ou d'un renversement, on regarde ce que joue le hautbois : dans un mouvement conjoint (succession d'intervalles de secondes), il est difficile de distinguer les notes de l'arpège des notes de passage, mais
: les notes des grands intervalles font partie de l'accord.
Ainsi, sur le premier temps de la première mesure (la basse est un ''mi''♭), on a une sixte descendante ''sol''-''si''♭ et, à la fin du temps, une tierce descendante ''sol''-''mi''♭. L'accord est donc ''mi''♭-''sol''-''si''♭, c'est un accord de quinte (accord parfait à l'état fondamental). À la fin du premier temps, le basson joue un ''do'', c'est donc une note étrangère.
Sur le second temps de la première mesure, le basson joue une tierce ascendante ''fa''-''la''♭, la première note est la basse de l'accord et la seconde une des notes de l'accord. Le hautbois commence par une sixte descendante ''la''♭-''do'', l'accord est donc ''fa''-''la''♭-''do'', un accord de quinte (accord parfait à l'état fondamental). Le ''do'' du basson la fin du premier temps est donc une anticipation.
Les autres notes étrangères de la première mesure sont des notes de passage.
Mais il faut faire attention : en suivant ce principe, sur les premiers temps des deuxième et troisième mesure, nous aurions des accords de septième d'espèce (puisque la septième est majeure). Or, on ne trouve pas, ou alors exceptionnellement, d'accord de septième d'espèce dans le baroque, mais quasi exclusivement des accords de septième de dominante. Donc au début de la deuxième mesure, le ''la''♮ est une appoggiature du ''si''♭, l'accord est donc ''si''♭-''ré''-''fa'', un asscord de quinte. De même, au début de la troisième mesure, le ''sol'' est une appoggiature du ''la''♭.
Il faut donc se méfier d'une analyse purement « mathématique ». Il faut s'attacher à ressentir la musique, et à connaître les styles, pour faire une analyse pertinente.
Ci-dessous, nous avons grisé les notes étrangères.
[[Fichier:Sonate hautbois basson heinichen 2e mvt mes49 analyse.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen. Analyse de la progression harmonique.]]
Le chiffrage jazz équivalent est :
: | E♭ F– | B♭<sup>Δ</sup> E♭ | A♭<sup>Δ</sup> D– | G …
Nous remarquons une progression assez régulière :
: ''mi''♭ ↗[2<sup>de</sup>] ''fa'' | ↘[5<sup>te</sup>] ''si''♭ ↗[4<sup>te</sup>] ''mi''♭ | ↘[5<sup>te</sup>] ''la''♭ ↗[4<sup>te</sup>] ''ré'' | ↘[5<sup>te</sup>] ''sol''
Le ''mi''♭ est le degré {{Times New Roman|III}} de la tonalité principale (''do'' mineur), c'est donc une tonique faible ; il « joue le même rôle » qu'un ''do''. S'il y avait eu un accord de ''do'' au début de l'extrait, on aurait eu une progression parfaitement régulière ↗[4<sup>te</sup>] ↘[5<sup>te</sup>].
Nous avons les modulations suivantes :
* mesure 49 : ''do'' mineur naturel (le ''si''♭ n'est pas une sensible) avec un accord sur “{{Times New Roman|I}}” (tonique faible, {{Times New Roman|III}}, pour la première analyse, ou bien tonique forte, {{Times New Roman|I}}, pour la seconde) suivi d'un accord sur {{Times New Roman|IV}} ;
* mesure 50 : ''si''♭ majeur avec un accord sur {{Times New Roman|I}} suivi d'un accord sur {{Times New Roman|IV}} ;
* mesure 51 : ''la''♭ majeur avec un accord sur {{Times New Roman|I}}, et emprunt à ''do'' majeur avec un accord sur {{Times New Roman|II}} ({{Times New Roman|IV}} faible).
On a donc une marche harmonique {{Times New Roman|I}} → {{Times New Roman|IV}} qui descend d'une seconde majeure (un ton) à chaque mesure (''do'' → ''si''♭ → ''la''♭), avec une exception sur la dernière mesure (modulation en cours de mesure et descente d'une seconde mineure au lieu de majeure).
Ce passage est donc construit sur une régularité, une règle qui crée un effet d'attente — enchaînement {{Times New Roman|I}}<sup>5</sup> → {{Times New Roman|IV}}<sup>5</sup> avec une marche harmonique d'une seconde majeure descendante —, et des « surprises », des exceptions au début — ce n'est pas un accord {{Times New Roman|I}}<sup>5</sup> mais un accord {{Times New Roman|III}}<sup>5</sup> — et à la fin — modulation en milieu de mesure et dernière descente d'une seconde mineure (½t ''la''♭ → ''sol'').
L'extrait ne permet pas de le deviner, mais la mesure 52 est un retour en ''do'' mineur, avec donc une modulation sur la dominante (accord de ''sol''<sup>7</sup><sub>+</sub>, G<sup>7</sup>).
{{boîte déroulante/fin}}
== Progression d'accords ==
Comme pour la mélodie, la succession des accords dans un morceau, la progression d'accords, suit des règles. Et comme pour la mélodie, les règles diffèrent d'un style musical à l'autre et la créativité consiste à parfois ne pas suivre ces règles. Et comme pour la mélodie, on part d'un ensemble de notes organisé, d'une gamme caractéristique d'une tonalité, d'un mode.
Les accords les plus utilisés pour une tonalité donnée sont les accords dont la fondamentale sont les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la tonalité, en particulier la triade {{Times New Roman|I}}, appelée « accord parfait » ou « accord de tonique », et l'accord de septième {{Times New Roman|V}}, appelé « septième de dominante ».
Le fait d'avoir une progression d'accords qui se répète permet de structurer un morceau. Pour les morceaux courts, il participe au plaisir de l'écoute et facilite la mémorisation (par exemple le découpage couplet-refrain d'une chanson). Sur les morceaux longs, une trop grande régularité peut introduire de la lassitude, les longs morceaux sont souvent découpés en parties présentant chacune une progression régulière. Le fait d'avoir une progression régulière permet la pratique de l'improvisation : cadence en musique classique, solo en jazz et blues.
; Note
: Le terme « cadence » désigne plusieurs choses différentes, et notamment en harmonie :
:* une partie improvisée dans un opéra ou un concerto, sens utilisé ci-dessus ;
:* une progression d'accords pour ponctuer un morceau et en particulier pour le conclure, sens utilisé dans la section suivante.
=== Accords peu utilisés ===
En mode mineur, l'accord de quinte augmentée {{Times New Roman|III<sup>+5</sup>}} est très peu utilisé. C'est un accord dissonant ; il intervient en général comme appogiature de l'accord de tonique (par exemple en ''la'' mineur : {{Times New Roman|III<sup>+5</sup>}} ''do'' - ''mi'' - ''sol''♯ → {{Times New Roman|I<sup>6</sup>}} ''do'' - ''mi'' - ''la''), ou de l'accord de dominante ({{Times New Roman|III<sup>6</sup><sub>+3</sub>}} ''mi'' - ''sol''♯ - ''do'' → {{Times New Roman|V<sup>5</sup>}} ''mi'' - ''sol''♯ - ''si''). Il peut être aussi utilisé comme préparation à l'accord de sous-dominante (enchaînement {{Times New Roman|III}} → {{Times New Roman|IV}}). Par ailleurs, il a une constitution symétrique — c'est l'empilement de deux tierces majeures — et ses renversements ont les mêmes intervalles à l'enharmonie près (quinte augmentée/sixte mineure, tierce majeure/quarte diminuée). De ce fait, un même accord est commun, par renversement et à l'enharmonie près, à trois tonalités : le premier renversement de l'accord ''do'' - ''mi'' - ''sol''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur) est enharmonique à ''mi'' - ''sol''♯ - ''si''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''do''♯ mineur) ; le second renversement est enharmonique à ''la''♭ - ''do'' - ''mi'' ({{Times New Roman|III}}<sup>e</sup> degré de ''fa'' mineur).
=== Accords très utilisés ===
Les trois accords les plus utilisés sont les accords de tonique (degré {{Times New Roman|I}}), de sous-dominante ({{Times New Roman|IV}}) et de dominante ({{Times New Roman|V}}). Ils interviennent en particulier en fin de phrase, dans les cadences. L'accord de dominante sert souvent à introduire une modulation : la modulation commence sur l'accord de dominante de la nouvelle tonalité. On note que l'accord de sous-dominante est situé une quinte juste en dessous de la tonique, les accords de dominante et de sous-dominante sont donc symétriques.
En jazz, on utilise également très fréquemment l'accord de la sus-tonique (degré {{Times New Roman|II}}), souvent dans des progressions {{Times New Roman|II}} - {{Times New Roman|V}} (- {{Times New Roman|I}}). Rappelons que l'accord de sus-tonique a la fonction de sous-dominante.
=== Cadences et ''turnaround'' ===
Le terme « cadence » provient de l'italien ''cadenza'' et désigne la « chute », la fin d'un morceau ou d'une phrase musicale.
On distingue deux types de cadences :
* les cadences conclusive, qui créent une sensation de complétude ;
* les cadences suspensives, qui crèent une sensation d'attente.
==== Cadence parfaite ====
[[Fichier:Au clair de le lune cadence parfaite.midi|thumb|''Au clair de la lune'', harmonisé avec une cadence parfaite (italienne).]]
[[Fichier:Au clair de le lune mineur cadence parfaite.midi|thumb|''Idem'' mais en mode mineur harmonique.]]
La cadence parfaite est l'enchaînement de l'accord de dominante suivi de l'accord parfait : {{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}}, les deux accord étant à l'état fondamental. Elle donne une impression de stabilité et est donc très souvent utilisée pour conclure un morceau. C'est une cadence conclusive.
On peut aussi utiliser l'accord de septième de dominante, la dissonance introduisant une tension résolue par l'accord parfait : {{Times New Roman|V<sup>7</sup><sub>+</sub> - I<sup>5</sup>}}.
Elle est souvent précédée de l'accord construit sur le IV<sup>e</sup> degré, appelé « accord de préparation », pour former la cadence italienne : {{Times New Roman|IV<sup>5</sup> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}}.
Elle est également souvent précédée du second renversement de l'accord de tonique, qui est alors appelé « appoggiature de la cadence » : {{Times New Roman|I<sup>6</sup><sub>4</sub> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}} (on remarque que les accords {{Times New Roman|I}}<sup>6</sup><sub>4</sub> et {{Times New Roman|V}}<sup>5</sup> ont la basse en commun, et que l'on peut passer de l'un à l'autre par un mouvement conjoint sur les autres notes).
{{clear}}
==== Demi-cadence ====
[[Fichier:Au clair de le lune demi cadence.midi|thumb|''Au clair de la lune'', harmonisé avec une demi-cadence.]]
Une demi-cadence est une phrase ou un morceau se concluant sur l'accord construit sur le cinquième degré. Il provoque une sensation d'attente, de suspens. Il s'agit en général d'une succession {{Times New Roman|II - V}} ou {{Times New Roman|IV - V}}. C'est une cadence suspensive. On uilise rarement un accord de septième de dominante.
{{clear}}
==== Cadence rompue ou évitée ====
La cadence rompue, ou cadence évitée, est succession d'un accord de dominante et d'un accord de sus-dominante, {{Times New Roman|V}} - {{Times New Roman|VI}}. C'est une cadence suspensive.
==== Cadence imparfaite ====
Une cadence imparfaite est une cadence {{Times New Roman|V - I}}, comme la cadence parfaite, mais dont au moins un des deux accords est dans un état renversé.
==== Cadence plagale ====
La cadence plagale — du grec ''plagios'', oblique, en biais — est la succession de l'accord construit sur le quatrième degré, suivi de l'accord parfait : {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}. Elle peut être utilisée après une cadence parfaite ({{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}} - {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}). Elle donne un caractère solennel, voire religieux — elle est parfois appelée « cadence amen » —, elle a un côté antique qui rappelle la musique modale et médiévale<ref>{{lien web |url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/la-cadence-amen-ou-comment-se-dire-adieu-7191921 |titre=La cadence « Amen » ou comment se dire adieu |auteur=Max Dozolme (MAXXI Classique) |site=France Musique |date=2025-04-25 |consulté le=2025-04-25}}.</ref>.
C'est une cadence conclusive.
==== {{lang|en|Turnaround}} ====
[[Fichier:Au clair de le lune turnaround.midi|thumb|Au clair de la lune, harmonisé en style jazz : accords de 7{{e}}, anatole suivie d'un ''{{lang|en|turnaround}}'' ii-V-I.]]
Le terme ''{{lang|en|turnaround}}'' signifie revirement, retournement. C'est une succession d'accords que fait la transition entre deux parties, en créant une tension-résolution. Le ''{{lang|en|turnaround}}'' le plus courant est la succession {{Times New Roman|II - V - I}}.
On utilise également fréquemment l'anatole : {{Times New Roman|I - VI - II - V}}.
{| class="wikitable"
|+ Progressions typiques d'accords dans une tonalité majeure
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Cadence<br />parfaite<br /> {{Times New Roman|V - I}}
! scope="col" | Cadence<br />italienne<br />{{Times New Roman|IV - V - I}}
! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou IV - V}}
! scope="col" | Cadence<br />plagale<br />{{Times New Roman|IV - I}}
! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}}
! scope="col" | Anatole<br />{{Times New Roman|I - vi - ii - V}}
|-
|''Do'' majeur || || G - C || F - G - C || Dm - G ou F - G || F - C || Dm - G - C || C - Am - Dm - G
|-
|''Sol'' majeur || ''fa''♯ || D - G || C - D - G || Am - D ou C - D || C - G || Am - D - G || G - Em - Am - D
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || A - D || G - A - D || Em - A ou G - A || G - D || Em - A - D || D - Bm - Em - A
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || E - A || D - E - A || Bm - E ou D - E || D - A || Bm - E - A || A - F♯m - B - E
|-
| ''Fa'' majeur || ''si''♭ || C - F || B♭ - C - F || Gm - C ou B♭ - C || B♭ - F || Gm - C - F || F - Dm - Gm - C
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || F - B♭ || E♭ - F - B♭ || Cm - F ou E♭ - F || E♭ - B♭ || Cm - F - B♭ || B♭ - Gm - Cm - F
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || B♭ - E♭ || A♭ - B♭ - E♭ || Fm - B♭ ou A♭ - B♭ || A♭ - E♭ || Fm - B♭ - E♭ || Gm - Cm - Fm - B♭
|}
{| class="wikitable"
|+ Progressions typiques d'accords dans une tonalité mineure
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Cadence<br />parfaite<br />{{Times New Roman|V - i}}
! scope="col" | Cadence<br />italienne<br />{{Times New Roman|iv - V - i}}
! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou iv - V}}
! scope="col" | Cadence<br />plagale<br />{{Times New Roman|iv - i}}
! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}}
! scope="col" | Anatole<br />{{Times New Roman|i - VI - ii - V}}
|-
| ''La'' mineur<br />harmonique || || E - Am || Dm - E - Am || B° - E ou Dm - E || Dm - Am || B° - E - Am || Am - F - B° - E
|-
| ''Mi'' mineur<br />harmonique || ''fa''♯ || B - Em || Am - B - Em || F♯° - B ou Am - B || Am - Em || F♯° - B - Em || Em - C - F♯° - B
|-
| ''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || F♯ - Bm || Em - F♯ - Bm || C♯° - F♯ ou Em - F♯ || Em - Bm || C♯° - F♯ - Bm || Bm - G - C♯° - F♯
|-
| ''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || C♯ - F♯m || Bm - C♯ - F♯m || G♯° - C♯ ou Bm - C♯ || Bm - F♯m || G♯° - C♯ - F♯m || A+ - D - G♯° - C♯
|-
| ''Ré'' mineur<br />harmonique || ''si''♭ || A - Dm || Gm - A - Dm || E° - A ou Gm - A || Gm - Dm || E° - A - Dm || Dm - B♭ - E° - A
|-
| ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || D - Gm || Cm - D - Gm || A° - D ou Cm - D || Cm - Gm|| A° - D - Gm || Gm - E♭ - A° - D
|-
| ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || G - Cm || Fm - G - Cm || D° - G ou Fm - G || Fm - Dm || D° - G - Cm || Cm - A♭ - D° - G
|}
==== Exemple : ''La Mer'' ====
: {{lien web
| url = https://www.youtube.com/watch?v=PXQh9jTwwoA
| titre = Charles Trenet - La mer (Officiel) [Live Version]
| site = YouTube
| auteur = Charles Trenet
| consulté le = 2020-12-24
}}
Le début de ''La Mer'' (Charles Trenet, 1946) est en ''do'' majeur et est harmonisé par l'anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (C - Am - Dm - G<sup>7</sup>) sur deux mesures, jouée deux fois ({{Times New Roman|1=<nowiki>|I-vi|ii-V</nowiki><sup>7</sup><nowiki>|</nowiki>}} × 2). Viennent des variations avec les progressions {{Times New Roman|I-III-vi-V<sup>7</sup>}} (C - E - Am - G<sup>7</sup>) puis la « progression ’50s » (voir plus bas) {{Times New Roman|I-vi-IV-VI<sup>7</sup>}} (C - Am - F - A<sup>7</sup>, on remarque que {{Times New Roman|IV}}/F est le relatif majeur du {{Times New Roman|ii}}/Dm de l'anatole), jouées chacune une fois sur deux mesure ; puis cette première partie se conclut par une demie cadence {{Times New Roman|ii-V<sup>7</sup>}} sur une mesure puis une dernière anatole sur trois mesures ({{Times New Roman|1=<nowiki>|I-vi|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}}). Cela constitue une première partie « A » sur douze mesures qui se termine par une demi-cadence ({{Times New Roman|ii-V<sup>7</sup>}}) qui appelle une suite. Cette partie A est jouée une deuxième fois mais la fin est modifiée pour la transition : les deux dernières mesures {{Times New Roman|<nowiki>|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}} deviennent {{Times New Roman|<nowiki>|ii-V</nowiki><sup>7</sup><nowiki>|I|</nowiki>}} (|Dm-G7|C|), cette partie « A’ » se conclut donc par une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}).
Le morceau passe ensuite en tonalité de ''mi'' majeur, donc une tierce au dessus de ''do'' majeur, sur six mesures. Cette partie utilise une progression ’50s {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (E - C♯m - A - B<sup>7</sup>), qui est rappelons-le une variation de l'anatole, l'accord {{Times New Roman|ii}} (Fm) étant remplacé par son relatif majeur {{Times New Roman|IV}} (A). Cette anatole modifiée est jouée deux fois puis la partie en ''mi'' majeur se conclut par l'accord parfait {{Times New Roman|I}} joué sur deux mesures (|E|E|), on a donc, avec la mesure précédente, avec une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}).
Suivent ensuite six mesures en ''sol'' majeur, donc à nouveau une tierce au dessus de ''mi'' majeur. Elle comporte une progression {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (G - Em - C - D<sup>7</sup>), donc anatole avec substitution du {{Times New Roman|ii}}/Am par son relatif majeur {{Times New Roman|VI}}/C (progression ’50s), puis une anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (G - Em - Am - D<sup>7</sup>) et deux mesure sur la tonique {{Times New Roman|I-I<sup>7</sup>}} (G - G<sup>7</sup>), formant à nouveau une cadence parfaite. La fin sur un accord de septième, dissonant, appelle une suite.
Cette partie « B » de douze mesures comporte donc deux parties similaires « B1 » et « B2 » qui forment une marche harmonique (montée d'une tierce).
Le morceau se conclut par une reprise de la partie « A’ » et se termine donc par une cadence parfaite.
Nous avons une structure A-A’-B-A’ sur 48 mesures, proche la forme AABA étudiée plus loin.
Donc ''La Mer'' est un morceau structuré autour de l'anatole avec des variations (progression ’50s, substitution du {{Times New Roman|ii}} par son relatif majeur {{Times New Roman|IV}}) et comportant une marche harmonique dans sa troisième partie. Les parties se concluent par des ''{{lang|en|turnarounds}}'' sous la forme d'une cadence parfaite ou, pour la partie A, par une demi-cadence.
{| border="1" rules="rows" frame="hsides"
|+ Structure de ''La Mer''
|- align="center"
|
| colspan="12" | ''do'' majeur
|
|- align="center"
! scope="row" rowspan=2 | A
| colspan="2" | anatole
| colspan="2" | //
| colspan="2" | variation
| colspan="2" | ’50s
| ½ c.
| colspan="3" | anatole
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii}} || <nowiki>|</nowiki> {{Times New Roman|V<sup>7</sup>}} || <nowiki>|</nowiki>
|- align="center"
! scope="row" rowspan="2" | A’
| colspan="2" | anatole
| colspan="2" | //
| colspan="2" | variation
| colspan="2" | ’50s
| ½ c.
| colspan="2" | anatole
| c.p.
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki>
|- align="center"
|
| colspan="6" | B1 : ''mi'' majeur
| colspan="6" background="lightgray" | B2 : ''sol'' majeur
|
|- align="center"
! scope="row" rowspan="2" | B
| colspan="2" | ’50s
| colspan="2" | //
|colspan="2" | c.p.
| colspan="2" | ’50s
| colspan="2" | //
|colspan="2" | c.p.
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I<sup>7</sup>}} || <nowiki>|</nowiki>
|-
! scope="row" | A’
| colspan="12" |
|
|}
=== Progression blues ===
La musique blues est apparue dans les années 1860. Elle est en général bâtie sur une grille d'accords ''({{lang|en|changes}})'' immuable de douze mesures ''({{lang|en|twelve-bar blues}})''. C'est sur cet accompagnement qui se répète que s'ajoute la mélodie — chant et solo. Cette structure est typique du blues et se retrouve dans ses dérivés comme le rock 'n' roll.
Le rythme est toujours un rythme ternaire syncopé ''({{lang|en|shuffle, swing, groove}}, ''notes inégales'')'' : la mesure est à quatre temps, mais la noire est divisée en noire-croche en triolet, ou encore triolet de croche en appuyant la première et la troisième.
La mélodie se construit en général sur une gamme blues de six degrés (gamme pentatonique mineure avec une quarte augmentée), mais bien que la gamme soit mineure, l'harmonie est construite sur la gamme majeure homonyme : un blues en ''fa'' a une mélodie sur la gamme de ''fa'' mineur, mais une harmonie sur la gamme de ''fa'' majeur. La grille d'accord comporte les accords construits sur les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la gamme majeure homonyme. Les accords sont souvent des accords de septième (donc avec une tierce majeure et une septième mineure), il ne s'agit donc pas d'une harmonisation de gamme diatonique (puisque la septième est majeure sur l'accord de tonique).
Par exemple, pour un blues en ''do'' :
* accord parfait de do majeur, C ({{Times New Roman|I}}<sup>er</sup> degré) ;
* accord parfait de fa majeur, F ({{Times New Roman|IV}}<sup>e</sup> degré) ;
* accord parfait de sol majeur, G ({{Times New Roman|V}}<sup>e</sup> degré).
Il existe quelques morceaux harmonisés avec des accords mineurs, comme par exemple ''As the Years Go Passing By'' d'Albert King (Duje Records, 1959).
La progression blues est organisée en trois blocs de quatre mesures ayant les fonctions suivantes (voir ci-dessus ''[[#Harmonie fonctionnelle|Harmonie fonctionnelle]]'') :
* quatre mesures toniques ;
* quatre mesures sous-dominantes ;
* quatre mesures dominantes.
La forme la plus simple, que Jeff Gardner appelle « forme A », est la suivante :
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, forme A
|-
! scope="row" | Tonique
| width="50px" | I
| width="50px" | I
| width="50px" | I
| width="50px" | I
|-
! scope="row" | Sous-domminante
| width="50px" | IV
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
! scope="row" | Dominante
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | V
|}
La progression {{Times New Roman|I-V}} des deux dernières mesures forment le ''{{lang|en|turnaround}}'', la demie cadence qui lance le cycle suivant. Nous présentons ci-dessous un exemple typique de ligne de basse ''({{lang|en|walking bass}})'' pour le ''{{lang|en|turnaround}}'' d'un blues en ''la'' :
[[Fichier:Turnaround classique blues en la.svg|Exemple typique de ligne de basse pour un ''turnaround'' de blues en ''la''.]]
[[Fichier:Blues mi harmonie elementaire.midi|thumb|Blues en ''mi'', harmonisé de manière élémentaire avec une ''{{lang|en|walking bass}}''.]]
Vous pouvez écouter ci-contre une harmonisation typique d'un blues en ''mi''. Les accords sont exécutés par une basse marchante ''({{lang|en|walking bass}})'', qui joue une arpège sur la triade avec l'ajout d'une sixte majeure et d'une septième mineure, et par une guitare qui joue un accord de puissance ''({{lang|en|power chord}})'', qui n'est composé que de la fondamentale et de la quinte juste, avec une sixte en appoggiature.
La forme B s'obtient en changeant la deuxième mesure : on joue un degré {{Times New Roman|IV}} au lieu d'un degré {{Times New Roman|I}}. La progression {{Times New Roman|I-IV}} sur les deux premières mesures est appelé ''{{lang|en|quick change}}''.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, forme B
|-
| width="50px" | I
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
| width="50px" | IV
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | V
|}
Par exemple, ''Sweet Home Chicago'' (Robert Johnson, 1936) est un blues en ''fa'' ; sa grille d'accords, aux variations près, suit une forme B :
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression de ''Sweet Home Chicago''
|-
| width="50px" | F
| width="50px" | B♭
| width="50px" | F
| width="50px" | F
|-
| width="50px" | B♭
| width="50px" | B♭
| width="50px" | F
| width="50px" | F
|-
| width="50px" | C7
| width="50px" | B♭7
| width="50px" | F7
| width="50px" | C7
|}
: Écouter {{lien web
| url =https://www.youtube.com/watch?v=dkftesK2dck
| titre = Robert Johnson "Sweet Home Chicago"
| auteur = Michal Angel
| site = YouTube
| date = 2007-12-09 | consulté le = 2020-12-17
}}
Les formes C et D s'obtiennent à partir des formes A et B en changeant le dernier accord par un accord sur le degré {{Times New Roman|I}}, ce qui forme une cadence plagale.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, formes C et D
|-
| colspan="4" | …
|-
| colspan="4" | …
|-
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|}
L'harmonie peut être enrichie, notamment en jazz. Voici par exemple une grille du blues souvent utilisés en bebop.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Exemple de progression de blues bebop sur une base de forme B
|-
| width="60px" | I<sup>7</sup>
| width="60px" | IV<sup>7</sup>
| width="60px" | I<sup>7</sup>
| width="60px" | V–<sup>7</sup> <nowiki>|</nowiki> I<sup>7</sup>
|-
| width="60px" | IV<sup>7</sup>
| width="60px" | IV<sup>7</sup>
| width="60px" | I<sup>7</sup>
| width="60px" | VI<sup>7 ♯9 ♭13</sup>
|-
| width="60px" | II–<sup>7</sup>
| width="60px" | V<sup>7</sup>
| width="60px" | V<sup>7</sup> <nowiki>|</nowiki> IV<sup>7</sup>
| width="60px" | II–<sup>7</sup> <nowiki>|</nowiki> V<sup>7</sup>
|}
On peut aussi trouver des blues sur huit mesures, sur seize mesures comme ''Watermelon Man'' de Herbie Hancock (album ''Takin' Off'', Blue Note, 1962) ou ''Let's Dance'' de Jim Lee (interprété par Chris Montez, Monogram, 1962)
* {{lien web
|url= https://www.dailymotion.com/video/x5iduwo
|titre=Herbie Hancock - Watermelon Man (1962)
|auteur=theUnforgettablesTv
|site=Dailymotion
|date=2003 |consulté le=2021-02-09
}}
* {{lien web
|url=https://www.youtube.com/watch?v=6JXshurYONc
|titre=Let's Dance
|auteur=Chris Montez
|site=YouTube
|date=2016-08-06 |consulté le=2021-02-09
}}
À l'inverse, certains blues peuvent avoir une structure plus simple que les douze mesure ; par exemple ''Hoochie Coochie Man'' de Willie Dixon (interprété par Muddy Waters sous le titre ''Mannish Boy'', Chicago Blues, 1954) est construit sur un seul accord répété tout le long de la chanson.
* {{lien web
|url=https://www.dailymotion.com/video/x5iduwo
|titre=Muddy Waters - Hoochie Coochie Man
|auteur=Muddy Waters
|site=Dailymotion
|date=2012 | consulté le=2021-02-09
}}
=== Cadence andalouse ===
La cadence andalouse est une progression de quatre accords, descendant par mouvement conjoint :
* en mode de ''mi'' (mode phrygien) : {{Times New Roman|IV}} - {{Times New Roman|III}} - {{Times New Roman|II}} - {{Times New Roman|I}} ;<br />par exemple en ''mi'' phrygien : Am - G - F - E ; en ''do'' phrygien : Fm - E♭ - D♭ - C ;<br />on notera que le degré {{Times New Roman|III}} est diésé dans l'accord final (ou bécarre s'il est bémol dans la tonalité) ;
* en mode mineur : {{Times New Roman|I}} - {{Times New Roman|VII}} - {{Times New Roman|VI}} - {{Times New Roman|V}} ;<br />par exemple en ''la'' mineur : Am - G - F - E ; en ''do'' mineur : Cm - B♭ - A♭ - m ;<br />comme précédemment, on notera que le degré {{Times New Roman|VII}} est diésé dans l'accord final.
=== Progressions selon le cercle des quintes ===
[[Fichier:Cercle quintes degres tonalite majeure.svg|vignette|Cercle des quinte justes (parcouru dans le sens des aiguilles d'une montre) des degrés d'une tonalité majeure.]]
La progression {{Times New Roman|V-I}} est la cadence parfaite, mais on peut aussi l'employer au milieu d'un morceau. Cette progression étant courte, sa répétition crée de la lassitude ; on peut la compléter par d'autres accords séparés d'une quinte juste, en suivant le « cercle des quintes » : {{Times New Roman|I-V-IX}}, la neuvième étant enharmonique de la seconde, on obtient {{Times New Roman|I-V-II}}.
On peut continuer de décrire le cercle des quintes : {{Times New Roman|I-V-II-VI}}, on obtient l'anatole dans le désordre ; on peut à l'inverse étendre les quintes vers la gauche, {{Times New Roman|IV-I-V-II-VI}}.
En musique populaire, on trouve fréquemment une progression fondée sur les accord {{Times New Roman|I}}, {{Times New Roman|IV}}, {{Times New Roman|V}} et {{Times New Roman|VI}}, popularisée dans les années 1950. La « progression années 1950 », « progression ''{{lang|en|fifties ('50)}}'' » ''({{lang|en|'50s progression}})'' est dans l'ordre {{Times New Roman|I-VI-IV-V}}. On trouve aussi cette progression en musique classique. Si la tonalité est majeure, la triade sur la sus-dominante est mineure, les autres sont majeures, on notera donc souvent {{Times New Roman|I-vi-IV-V}}. On peut avoir des permutations circulaires (le dernier accord venant au début, ou vice-versa) : {{Times New Roman|vi-IV-V-I}}, {{Times New Roman|IV-V-I-vi}} et {{Times New Roman|V-I-vi-IV}}.
{| class="wikitable"
|+ Accords selon la tonalité
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" style="font-family:Times New Roman" | I
! scope="col" style="font-family:Times New Roman" | IV
! scope="col" style="font-family:Times New Roman" | V
! scope="col" style="font-family:Times New Roman" | vi
|-
|''Do'' majeur || || C || F || G || Am
|-
|''Sol'' majeur || ''fa''♯ || G || C || D || Em
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || D || G || A || Bm
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A || D || E || F♯m
|-
| ''Fa'' majeur || ''si''♭ || F || B♭ || C || Dm
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭ || E♭ || F || Gm
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭ || A♭ || B♭ || Cm
|}
Par exemple, en tonalité de ''do'' majeur, la progression {{Times New Roman|I-vi-IV-V}} sera C-Am-F-G.
Il existe d'autres progressions utilisant ces accords mais dans un autre ordre, typiquement {{Times New Roman|I–IV–vi–V}} ou une de ses permutations circulaires : {{Times New Roman|IV–vi–V-I}}, {{Times New Roman|vi–V-I-IV}} ou {{Times New Roman|V-I-IV-vi}}. Ou dans un autre ordre.
PV Nova l'illustre dans plusieurs de ses « expériences » dans la version {{Times New Roman|vi-V-IV-I}}, soit Am-G-F-C, ou encore {{Times New Roman|vi-IV-I-V}}, soit Am-F-C-G :
: {{lien web
| url = https://www.youtube.com/watch?v=w08LeZGbXq4
| titre = Expérience n<sup>o</sup> 6 — La Happy Pop
| auteur = PV Nova
| site = YouTube
| date = 2011-08-20 | consulté le = 2020-12-13
}}
et cela devient un gag récurrent avec son « chapeau des accords magiques qu'on nous ressort à toutes les sauces »
: {{lien web
| url = https://www.youtube.com/watch?v=VMY_vc4nZAU
| titre = Expérience n<sup>o</sup> 14 — La Soupe dou Brasil
| auteur = PV Nova
| site = YouTube
| date = 2012-10-03 | consulté le = 2020-12-17
}}
Cette récurrence est également parodiée par le groupe The Axis of Awesome avec ses « chansons à quatre accords » ''({{lang|en|four-chords song}})'', dans une sketch où ils mêlent 47 chansons en utilisant l'ordre {{Times New Roman|I-V-vi-IV}} :
: {{lien web
| url = https://www.youtube.com/watch?v=oOlDewpCfZQ
| titre = 4 Chords | Music Videos | The Axis Of Awesome
| auteur = The Axis of Awesome
| site = YouTube
| date = 2011-07-20 | consulté le = 2020-12-17
}}
{{boîte déroulante/début|titre=Chansons mêlées dans le sketch}}
# Journey : ''Don't Stop Believing'' ;
# James Blunt : ''You're Beautiful'' ;
# Black Eyed Peas : ''Where Is the Love'' ;
# Alphaville : ''Forever Young'' ;
# Jason Mraz : ''I'm Yours'' ;
# Train : ''Hey Soul Sister'' ;
# The Calling : ''Wherever You Will Go'' ;
# Elton John : ''Can You Feel The Love Tonight'' (''Le Roi lion'') ;
# Akon : ''Don't Matter'' ;
# John Denver : ''Take Me Home, Country Roads'' ;
# Lady Gaga : ''Paparazzi'' ;
# U2 : ''With Or Without You'' ;
# The Last Goodnight : ''Pictures of You'' ;
# Maroon Five : ''She Will Be Loved'' ;
# The Beatles : ''Let It Be'' ;
# Bob Marley : ''No Woman No Cry'' ;
# Marcy Playground : ''Sex and Candy'' ;
# Men At Work : ''Land Down Under'' ;
# thème de ''America's Funniest Home Videos'' (équivalent des émissions ''Vidéo Gag'' et ''Drôle de vidéo'') ;
# Jack Johnson : ''Taylor'' ;
# Spice Girls : ''Two Become One'' ;
# A Ha : ''Take On Me'' ;
# Green Day : ''When I Come Around'' ;
# Eagle Eye Cherry : ''Save Tonight'' ;
# Toto : ''Africa'' ;
# Beyonce : ''If I Were A Boy'' ;
# Kelly Clarkson : ''Behind These Hazel Eyes'' ;
# Jason DeRulo : ''In My Head'' ;
# The Smashing Pumpkins : ''Bullet With Butterfly Wings'' ;
# Joan Osborne : ''One Of Us'' ;
# Avril Lavigne : ''Complicated'' ;
# The Offspring : ''Self Esteem'' ;
# The Offspring : ''You're Gonna Go Far Kid'' ;
# Akon : ''Beautiful'' ;
# Timberland featuring OneRepublic : ''Apologize'' ;
# Eminem featuring Rihanna : ''Love the Way You Lie'' ;
# Bon Jovi : ''It's My Life'' ;
# Lady Gaga : ''Pokerface'' ;
# Aqua : ''Barbie Girl'' ;
# Red Hot Chili Peppers : ''Otherside'' ;
# The Gregory Brothers : ''Double Rainbow'' ;
# MGMT : ''Kids'' ;
# Andrea Bocelli : ''Time To Say Goodbye'' ;
# Robert Burns : ''Auld Lang Syne'' ;
# Five for fighting : ''Superman'' ;
# The Axis of Awesome : ''Birdplane'' ;
# Missy Higgins : ''Scar''.
{{boîte déroulante/fin}}
Vous pouvez par exemple jouer les accords C-G-Am-F ({{Times New Roman|I-V-vi-IV}}) et chanter dessus ''{{lang|en|Let It Be}}'' (Paul McCartney, The Beattles, 1970) ou ''Libérée, délivrée'' (Robert Lopez, ''La Reine des neiges'', 2013).
La progression {{Times New Roman|I-V-vi-IV}} est considérée comme « optimiste » tandis que sa variante {{Times New Roman|iv-IV-I-V}} est considérée comme « pessimiste ».
On peut voir la progression {{Times New Roman|I-vi-IV-V}} comme une variante de l'anatole {{Times New Roman|I-vi-ii-V}}, obtenue en remplaçant l'accord de sustonique {{Times New Roman|ii}} par l'accord de sous-dominante {{Times New Roman|IV}} (son relatif majeur, et degré ayant la même fonction).
==== Exemples de progression selon le cercle des quintes en musique classique ====
[[Fichier:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.midi|vignette|Dietrich Buxtehude, Psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]]
Cette progression selon la cercle des quintes, sous la forme {{Times New Roman|I-vi-IV-V}}, apparaît déjà au {{pc|xvii}}<sup>e</sup> siècle dans le psaume 42 ''Quem ad modum desiderat cervis'' (BuxVW92) de Dietrich Buxtehude (1637-1707). Le morceau est en ''fa'' majeur, la progression d'accords est donc F-Dm-B♭-C.
: {{lien web
| url = https://www.youtube.com/watch?v=8FmV9l1RqSg
| titre = D. Buxtehude - Quemadmodum desiderat cervus, BuxWV 92
| auteur = Longobardo
| site = YouTube
| date = 2013-04-06 | consulté la = 2021-01-01
}}
[[File:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.svg|vignette|450x450px|center|Dietrich Buxtehude, psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]]
{{clear}}
[[Fichier:JSBach BWV140 cantate 4 mesures.midi|vignette|J.-S. Bach, cantate BWV140, quatre premières mesures.]]
On la trouve également dans l'ouverture de la cantate ''{{lang|de|Wachet auf, ruft uns die Stimme}}'' de Jean-Sébastien Bach (BWV140, 1731). Le morceau est en ''mi''♭ majeur, la progression d'accords est donc E♭-Cm-A♭<sup>6</sup>-B♭.
[[Fichier:JSBach BWV140 cantate 4 mesures.svg|vignette|center|J.-S. Bach, cantate BWV140, quatre premières mesures.|alt=|517x517px]]
{{clear}}
[[Fichier:Mozart K310 Sonate8 mesures 45 a 49.midi|vignette|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]]
La même progression est utilisée par Mozart, par exemple dans le premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778), la progression d'accords est C-Am-F-G qui correspond à la progression {{Times New Roman|III-i-VI-VII}} de ''la'' mineur, mais à la progression {{Times New Roman|I-vi-IV-V}} de la gamme relative, ''do'' majeur .
[[Fichier:Mozart K310 Sonate8 mesures 45 a 49.svg|vignette|center|500px|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]]
=== Substitution tritonique ===
Un des accords les plus utilisés est donc l'accord de septième de dominante, {{Times New Roman|V<sup>7</sup><sub>+</sub>}} qui contient les degrés {{Times New Roman|V}}, {{Times New Roman|VII}}, {{Times New Roman|II}} ({{Times New Roman|IX}}) et {{Times New Roman|IV}}({{Times New Roman|XI}}) ; par exemple, en tonalité de ''do'' majeur, l'accord de ''sol'' septième (G<sup>7</sup>) contient les notes ''sol''-''si''-''ré''-''fa''. Si l'on prend l'accord dont la fondamentale est trois tons (triton) au-dessus ou en dessous — l'octave contenant six tons, on arrive sur la même note —, {{Times New Roman|♭II<sup>7</sup>}}, ici ''ré''♭ septième (D♭<sup>7</sup>), celui-ci contient les notes ''ré''♭-''fa''-''la''♭-''do''♭, cette dernière note étant l'enharmonique de ''si''. Les deux accords G<sup>7</sup> et D♭<sup>7</sup> ont donc deux notes en commun : le ''fa'' et le ''si''/''do''♭.
Il est donc fréquent en jazz de substituer l'accord {{Times New Roman|V<sup>7</sup><sub>+</sub>}} par l'accord {{Times New Roman|♭II<sup>7</sup>}}. Par exemple, la progression {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|V<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}} devient {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|♭II<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}}. C'est un procédé courant de réharmonisation (le fait de remplacer un accord par un autre dans un morceau existant).
Les six substitutions possibles sont donc : C<sup>7</sup>↔F♯<sup>7</sup> - D♭<sup>7</sup>↔G<sup>7</sup> - D<sup>7</sup>↔A♭<sup>7</sup> - E♭<sup>7</sup>↔A<sup>7</sup> - E<sup>7</sup>↔B♭<sup>7</sup> - F<sup>7</sup>↔B<sup>7</sup>.
[[Fichier:Übermäsiger Terzquartakkord.jpg|vignette|Exemple de cadence parfaite en ''do'' majeur avec substitution tritonique (sixte française).]]
Dans l'accord D♭<sup>7</sup>, si l'on remplace le ''do''♭ par son ''si'' enharmonique, on obtient un accord de sixte augmentée : ''ré''♭-''fa''-''la''♭-''si''. Cet accord est utilisé en musique classique depuis la Renaissance ; on distingue en fait trois accords de sixte augmentée :
* sixte française ''ré''♭-''fa''-''sol''-''si'' ;
* sixte allemande : ''ré''♭-''fa''-''la''♭-''si'' ;
* sixte italienne : ''ré''♭-''fa''-''si''.
Par exemple, le ''Quintuor en ''ut'' majeur'' de Franz Schubert (1828) se termine par une cadence parfaite dont l'accord de dominante est remplacé par une sixte française ''ré''♭-''fa''-''si''-''sol''-''si'' (''ré''♭ aux violoncelles, ''fa'' à l'alto, ''si''-''sol'' aux seconds violons et ''si'' au premier violon).
[[Fichier:Schubert C major Quintet ending.wav|vignette|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]]
[[Fichier:Schubert C major Quintet ending.png|vignette|center|upright=2.5|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]]
=== Autres accords de substitution ===
Substituer un accord consiste à utiliser un accord provenant d'une tonalité étrangère à la tonalité en cours. À la différence d'une modulation, la substitution est très courte et ne donne pas l'impression de changer de tonalité ; on a juste un sentiment « étrange » passager. Un court passage dans une autre tonalité est également appelée « emprunt ».
Nous avons déjà vu plusieurs méthodes de substitution :
* utilisation d'une note étrangère : une note étrangère — note de passage, appoggiature, anticipation, retard… — crée momentanément un accord hors tonalité ; en musique classique, ceci n'est pas considéré comme un accord en propre, mais en jazz, on parle « d'accord de passage » et « d'accord suspendu » ;
* utilisation d'une dominante secondaire : l'accord de dominante secondaire est hors tonalité ; le but ici est de faire une cadence parfaite, mais sur un autre degré que la tonique de la tonalité en cours ;
* la substitution tritonique, vue ci-dessus, pour remplacer un accord de septième de dominante.
Une dernière méthode consiste à remplacer un accord par un accord d'une gamme de même tonique, mais d'un autre mode ; on « emprunte » ''({{lang|en|borrow}})'' l'accord d'un autre mode. Par exemple, substituer un accord de la tonalité de ''do'' majeur par un accord de la tonalité de ''do'' mineur ou de ''do'' mode de ''mi'' (phrygien).
Donc en ''do'' majeur, on peut remplacer un accord de ''ré'' mineur septième (D<sub>m</sub><sup>7</sup>) par un accord de ''ré'' demi-diminué (D<sup>⌀</sup>, D<sub>m</sub><sup>7♭5</sup>) qui est un accord appartenant à la donalité de ''la'' mineur harmonique.
=== Forme AABA ===
La forme AABA est composée de deux progressions de huit mesures, notées A et B ; cela représente trente-deux mesures au total, on parle donc souvent en anglais de la ''{{lang|en|32-bars form}}''. C'est une forme que l'on retrouve dans de nombreuses chanson de comédies musicales de Broadway comme ''Have You Met Miss Jones'' (''{{lang|en|I'd Rather Be Right}}'', 1937), ''{{lang|en|Over the Rainbow}}'' (''Le Magicien d'Oz'', Harold Harlen, 1939), ''{{lang|en|All the Things You Are}}'' (''{{lang|en|Very Warm for may}}'', 1939).
Par exemple, la version de ''{{lang|en|Over the Rainbow}}'' chantée par Judy Garland est en ''la''♭ majeur et la progression d'accords est globalement :
* A (couplet) : A♭-Fm | Cm-A♭ | D♭ | Cm-A♭ | D♭ | D♭-F | B♭-E♭ | A♭
* B (pont) : A♭ | B♭m | Cm | D♭ | A♭ | B♭-G | Cm-G | B♭m-E♭
soit en degrés :
* A : {{Times New Roman|<nowiki>I-vi | iii-I | IV | iii-IV | IV | IV-vi | II-V | I</nowiki>}}
* B : {{Times New Roman|<nowiki>I | ii | iii | IV | I | II-VII | iii-VII | ii-V</nowiki>}}
Par rapport aux paroles de la chanson, on a
* A : couplet 1 ''« {{lang|en|Somewhere […] lullaby}} »'' ;
* A : couplet 2 ''« {{lang|en|Somewhere […] really do come true}} »'' ;
* B : pont ''« {{lang|en|Someday […] you'll find me}} »'' ;
* A : couplet 3 ''« {{lang|en|Somewhere […] oh why can't I?}} »'' ;
: {{lien web
| url = https://www.youtube.com/watch?v=1HRa4X07jdE
| titre = Judy Garland - Over The Rainbow (Subtitles)
| site = YouTube
| auteur = Overtherainbow
| consulté le = 2020-12-17
}}
Une mise en œuvre de la forme AABA couramment utilisée en jazz est la forme anatole (à le pas confondre avec la succession d'accords du même nom), en anglais ''{{lang|en|rythm changes}}'' car elle s'inspire du morceau ''{{lang|en|I Got the Rythm}}'' de George Gerschwin (''Girl Crazy'', 1930) :
* A : {{Times New Roman|I–vi–ii–V}} (succession d'accords « anatole ») ;
* B : {{Times New Roman|III<sup>7</sup>–VI<sup>7</sup>–II<sup>7</sup>–V<sup>7</sup>}} (les fondamentales forment une succession de quartes, donc parcourent le « cercle des quintes » à l'envers).
Par exemple, ''I Got the Rythm'' étant en ''ré''♭ majeur, la forme est :
* A : D♭ - B♭m - E♭m - A♭
* B : F7 - B♭7 - E♭7 - A♭7
=== Exemples ===
==== Début du Largo de la symphonie du Nouveau Monde ====
[[File:Largo nouveau monde 5 1res mesures.svg|vignette|Partition avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]]
[[File:Largo nouveau monde 5 1res mesures.midi|vignette|Fichier son avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]]
Nous avons reproduit ci-contre les cinq premières mesure du deuxième mouvement Largo de la symphonie « Du Nouveau Monde » (symphonie n<sup>o</sup> 9 d'Antonín Dvořák, 1893). Cliquez sur l'image pour l'agrandir.
Vous pouvez écouter cette partie jouée par un orchestre symphonique :
* {{lien web
|url =https://www.youtube.com/watch?v=y2Nw9r-F_yQ?t=565
|titre = Dvorak Symphony No.9 "From the New World" Karajan 1966
|site=YouTube (Seokjin Yoon)
|consulté le=2020-12-11
}} (à 9 min 25), par le Berliner Philharmoniker, dirigé par Herbert von Karajan (1966) ;
* {{lien web
|url = https://www.youtube.com/watch?v=ASlch7R1Zvo
|titre=Dvořák: Symphony №9, "From The New World" - II - Largo
|site=YouTube (diesillamusicae)
|consulté le=2020-12-11
}} : Wiener Philharmoniker, dirigé par Herbert von Karajan (1985).
{{clear}}
Cette partie fait intervenir onze instruments monodiques (ne jouant qu'une note à la fois) : des vents (trois bois, sept cuivres) et une percussion. Certains de ces instruments sont transpositeurs (les notes sur la partition ne sont pas les notes entendues). Jouées ensemble, ces onze lignes mélodiques forment des accords.
Pour étudier cette partition, nous réécrivons les parties des instruments transpositeurs en ''do'' et les parties en clef d’''ut'' en clef de ''fa''. Nous regroupons les parties en clef de ''fa'' d'un côté et les parties en clef de ''sol'' d'un autre.
{{boîte déroulante|Résultat|contenu=[[File:Largo nouveau monde 5 1res mesures transpositeurs en do.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde, en do.]]}}
Nous pouvons alors tout regrouper sous la forme d'un système de deux portées clef de ''fa'' et clef de ''sol'', comme une partition de piano.
{{boîte déroulante|Résultat|contenu=
[[Fichier:Largo nouveau monde 5 1res mesures accords.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords.]]
}}
[[Fichier:Largo nouveau monde 5 1res mesures accords.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords.]]
{{clear}}
Ensuite, nous ne gardons que la basse et les notes médium. Nous changeons éventuellement certaines notes d'octave afin de n'avoir que des superpositions de tierce ou de quinte (état fondamental des accords, en faisant ressortir les notes manquantes).
{{boîte déroulante|Résultat|contenu=
[[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords simplifiés.]]
}}
[[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords simplifiés.]]
Vous pouvez écouter cette partie jouée par un quintuor de cuivres (trompette, bugle, cor, trombone, tuba), donc avec des accords de cinq notes :
: {{lien web
|url=https://www.youtube.com/watch?v=pWfe60nbvjA
|titre = Largo from The New World Symphony by Dvorak
|site=YouTube (The Chamberlain Brass)
|consulté le=2020-12-11
}} : The American Academy of Arts & Letters in New York City (2017).
Nous allons maintenant chiffrer les accords.
Pour établir la basse chiffrée, il nous faut déterminer le parcours harmonique. Pour le premier accord, les tonalités les plus simples avec un ''sol'' dièse sont ''la'' majeur et ''fa'' dièse mineur ; comme le ''mi'' est bécarre, nous retenons ''la'' majeur, il s'agit donc d'un accord de quinte sur la dominante (les accords de dominante étant très utilisés, cela nous conforte dans notre choix). Puis nous avons un ''si'' bémol, nous pouvons être en ''fa'' majeur ou en ''ré'' mineur ; nous retenons ''fa'' majeur, c'est donc le renversement d'un accord sur le degré {{Times New Roman|II}}.
Dans la deuxième mesure, nous revenons en ''la'' majeur, puis, avec un ''la'' et un ''ré'' bémols, nous sommes en ''la'' bémol majeur ; nous avons donc un accord de neuvième incomplet sur la sensible, ou un accord de onzième incomplet sur la dominante.
Dans la troisième mesure, nous passons en ''ré'' majeur, avec un accord de dominante. Puis, nous arrivons dans la tonalité principale, avec le renversement d'un accord de dominante sans tierce suivi d'un accord de tonique. Nous avons donc une cadence parfaite, conclusion logique d'une phrase.
La progression des accords est donc :
{| class="wikitable"
! scope="row" | Tonalité
| ''la'' M - ''fa'' M || ''la'' M - ''la''♭ M || ''ré'' M - ''ré''♭ M || ''ré''♭ M
|-
! scope="row" | Accords
| {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|II}}<sup>6</sup><sub>4</sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|“V”}}<sup>9</sup><sub><s>5</s></sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|V}}<sup>+4</sup> || {{Times New Roman|I}}<sup>5</sup>
|}
Dans le chiffrage jazz, nous avons donc :
* une triade de ''mi'' majeur, E ;
* une triade de ''sol'' majeur avec un ''ré'' en basse : G/D ;
* à nouveau un E ;
* un accord de ''sol'' neuvième diminué incomplet, avec un ''ré'' bémol en basse : G dim<sup>9</sup>/D♭ ;
* un accord de ''la'' majeur, A ;
* un accord de ''la'' bémol septième avec une ''sol'' bémol à la basse : A♭<sup>7</sup>/G♭ ;
* la partie se conclue par un accord parfait de ''ré''♭ majeur, D♭.
Soit une progression E - G/D | E - G dim<sup>9</sup>/D♭ | A - A♭<sup>7</sup>/G♭ | D♭.
[[Fichier:Largo nouveau monde 5 1res mesures accords chiffres.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde en accords simplifiés.]]
{{clear}}
==== Thème de Smoke on the Water ====
Le morceau ''Smoke on the Water'' du groupe Deep Purple (album ''Machine Head'', 1972) possède un célèbre thème, un riff ''({{lang|en|rythmic figure}})'', joué à la guitare sous forme d'accords de puissance ''({{lang|en|power chords}})'', c'est-à-dire des accords sans tierce. Le morceau est en tonalité de ''sol'' mineur naturel (donc avec un ''fa''♮) avec ajout de la note bleue (''{{lang|en|blue note}}'', quinte diminuée, ''ré''♭), et les accords composant le thème sont G<sup>5</sup>, B♭<sup>5</sup>, C<sup>5</sup> et D♭<sup>5</sup>, ce dernier accord étant l'accord sur la note bleue et pouvant être considéré comme une appoggiature (indiqué entre parenthèse ci-après). On a donc ''a priori'', sur les deux premières mesures, une progression {{Times New Roman|I-III-IV}} puis {{Times New Roman|I-III-(♭V)-IV}}. Durant la majeure partie du thème, la guitare basse tient la note ''sol'' en pédale.
{{note|En jazz, la qualité « <sup>5</sup> » indique que l'on n'a que la quinte (et donc pas la tierce), contrairement à la notation de basse chiffrée.}}
: {{lien web
| url = https://www.dailymotion.com/video/x5ili04
| titre = Deep Purple — Smoke on the Water (Live at Montreux 2006)
| auteur = Deep Purple
| site = Dailymotion
| date = 2016 | consulté le = 2020-12-31
}}
Cependant, cette progression forme une mélodie, on peut donc plus la voir comme un contrepoint, la superposition de deux voies ayant un mouvement conjoint, joué par un seul instrument, la guitare, la voie 2 étant jouée une quarte juste en dessous de la voie 1 (la quarte juste descendante étant le renversement de la quinte juste ascendante) :
* voie 1 (aigu) : | ''sol'' - ''si''♭ - ''do'' | ''sol'' - ''si''♭ - (''ré''♭) - ''do'' | ;
* voie 2 (grave) : | ''ré'' - ''fa'' - ''sol'' | ''ré'' - ''fa'' - (''la''♭) - ''sol'' |.
En se basant sur la basse (''sol'' en pédale), nous pouvons considérer que ces deux mesures sont accompagnées d'un accord de Gm<sup>7</sup> (''sol''-''si''♭-''ré''-''fa''), chaque accord de la mélodie comprenant à chaque fois au moins une note de cet accord à l'exception de l'appogiature.
{| class="wikitable"
|+ Mise en évidence des notes de l'accord Gm<sup>7</sup>
|-
! scope="row" | Accords
| G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup>
|-
! scope="row" | Voie 1
| '''''sol''''' || '''''si''♭''' || ''do''
|-
! scope="row" | Voie 2
| '''''ré''''' || '''''fa''''' || '''''sol'''''
|-
! scope="row" | Basse
| '''''sol''''' || '''''sol''''' || '''''sol'''''
|}
Sur les deux mesures suivantes, la basse varie et suit les accords de la guitare avec un retard sur le dernier accord :
{| class="wikitable"
|+ Voies sur les mesure 3-4 du thème
|-
! scope="row" | Accords
| G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> || B♭<sup>5</sup> || G<sup>5</sup>
|-
! scope="row" | Voie 1
| ''sol'' || ''si''♭ || ''do'' || ''si''♭ || ''sol''
|-
! scope="row" | Voie 2
| ''ré'' || ''fa'' || ''sol'' || ''fa'' || ''ré''
|-
! scope="row" | Basse
| ''sol'' || ''sol'' || ''do'' || ''si''♭ || ''si''♭-''sol''
|}
Le couplet de cette chanson est aussi organisé sur une progression de quatre mesures, la guitare faisant des arpèges sur les accords G<sup>5</sup> (''sol''-''ré''-''sol'') et F<sup>5</sup> (''fa''-''do''-''fa'') :
: | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-F<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> |
soit une progression {{Times New Roman|<nowiki>| I-I | I-I | I-VII | I-I |</nowiki>}}. Nous pouvons aussi harmoniser le riff du thème sur cette progression, avec un accord F (''fa''-''la''-''do'') ; nous pouvons aussi nous rappeler que l'accord sur le degré {{Times New Roman|VII}} est plus volontiers considéré comme un accord de septième de dominante {{Times New Roman|V<sup>7</sup>}}, soit ici un accord Dm<sup>7</sup> (''ré''-''fa''-''la''-''do''). On peut donc considérer la progression harmonique sur le thème :
: | Gm-Gm | Gm-Gm | Gm-F ou Dm<sup>7</sup> | Gm-Gm |.
Cette analyse permet de proposer une harmonisation enrichie du morceau, tout en se rappelant qu'une des forces du morceau initial est justement la simplicité de sa structure, qui fait ressortir la virtuosité des musiciens. Nous pouvons ainsi comparer la version album à la version concert avec orchestre ou à la version latino de Pat Boone. À l'inverse, le groupe Psychostrip, dans une version grunge, a remplacé les accords par une ligne mélodique :
* le thème ne contient plus qu'une seule voie (la guitare ne joue pas des accords de puissance) ;
* dans les mesures 9 et 10, la deuxième guitare joue en contrepoint de type mouvement inverse, qui est en fait la voie 2 jouée en miroir ;
* l'arpège sur le couplet est remplacé par une ligne mélodique en ostinato sur une gamme blues.
{| class="wikitable"
|+ Contrepoint sur les mesures 9 et 10
|-
! scope="row" | Guitare 1
| ''sol'' ↗ ''si''♭ ↗ ''do''
|-
! scope="row" | Guitare 2
| ''sol'' ↘ ''fa'' ↘ ''ré''
|}
* {{lien web
| url = https://www.dailymotion.com/video/x5ik234
| titre = Deep Purple — Smoke on the Water (In Concert with the London Symphony Orchestra, 1999)
| auteur = Deep Purple
| site = Dailymotion
| date = 2016 | consulté le = 2020-12-31
}}
* {{lien web
| url = https://www.youtube.com/watch?v=MtUuNzVROIg
| titre = Pat Boone — Smoke on the Water (In a Metal Mood, No More Mr. Nice Guy, 1997)
| auteur = Orrore a 33 Giri
| site = YouTube
| date = 2019-06-24 | consulté le = 2020-12-31
}}
* {{lien web
| url = https://www.youtube.com/watch?v=n7zLlZ8B0Bk
| titre = Smoke on the Water (Heroes, 1993)
| auteur = Psychostrip
| site = YouTube
| date = 2018-06-20 | consulté le = 2020-12-31
}}
== Accords et improvisation ==
Nous avons vu précédemment (chapitre ''[[../Gammes et intervalles#Modes et improvisation|Gammes et intervalles > Modes et improvisation]]'') que le choix d'un mode adapté permet d'improviser sur un accord. L'harmonisation des gammes permet, en inversant le processus, d'étendre notre palette : il suffit de repérer l'accord sur une harmonisaiton de gamme, et d'utiliser cette gamme-là, dans le mode correspondant du degré de l'accord (voir ci-dessus ''[[#Harmonisation par des accords de septième|Harmonisation par des accords de septième]]'').
Par exemple, nous avons vu que l'accord sur le septième degré d'une gamme majeure était un accord demi-diminué ; nous savons donc que sur un accord demi-diminué, nous pouvons improviser sur le mode correspondant au septième degré, soit le mode de ''si'' (locrien).
Un accord de septième de dominante étant commun aux deux tonalités homonymes (par exemple ''fa'' majeur et ''fa'' mineur pour un ''do''<sup>7</sup><sub>+</sub> / C<sup>7</sup>), nous pouvons utiliser le mode de ''sol'' de la gamme majeure (mixolydien) ou de la gamme mineure mineure (mode phrygien dominant, ou phrygien espagnol) pour improviser. Mais l'accord de septième de dominante est aussi l'accord au début d'une grille blues ; on peut donc improviser avec une gamme blues, même si la tierce est majeure dans l'accord et mineure dans la gamme.
[[Fichier:Mode improvisation accords do complet.svg]]
== Autres accords courants ==
[[fichier:Cluster cdefg.png|vignette|Agrégat ''do - ré - mi - fa - sol''.]]
Nous avons vu précédemment l'harmonisation des tonalités majeures et mineures harmoniques par des triades et des accords de septième ; certains accords étant rarement utilisés (l'accord sur le degré {{Times New Roman|III}} et, pour les tonalités mineures harmoniques, l'accord sur la tonique), certains accords étant utilisés comme des accords sur un autre degré (les accords sur la sensible étant considérés comme des accords de dominante sans fondamentale).
Dans l'absolu, on peut utiliser n'importe quelle combinaison de notes, jusqu'aux agrégats, ou ''{{lang|en|clusters}}'' (mot anglais signifiant « amas », « grappe ») : un ensemble de notes contigües, séparées par des intervalles de seconde. Dans la pratique, on reste souvent sur des accords composés de superpositions de tierces, sauf dans le cas de transitions (voir la section ''[[#Notes étrangères|Notes étrangère]]'').
=== En musique classique ===
On utilise parfois des accords dont les notes ne sont pas dans la tonalité (hors modulation). Il peut s'agir d'accords de passage, de notes étrangères, par exemple utilisant un chromatisme (mouvement conjoint par demi-tons).
Outre les accords de passage, les autres accords que l'on rencontre couramment en musique classique sont les accords de neuvième, et les accords de onzième et treizième sur tonique. Ces accords sont simplement obtenus en continuant à empiler les tierces. Il n'y a pas d'accord d'ordre supérieur car la quinzième est deux octaves au-dessus de la fondamentale.
Comme pour les accords de septième, on distingue les accords de neuvième de dominante et les accords de neuvième d'espèce. Dans le cas de la neuvième de dominante, il y a une différence entre les tonalités majeures et mineures : l'intervalle de neuvième est respectivement majeur et mineur. Les chiffrages des renversements peuvent donc différer. Comme pour les accords de septième de dominante, on considère que les accords de septième sur le degré {{Times New Roman|VI}} sont en fait des accords de neuvième de dominante sans fondamentale.
Les accords de neuvième d'espèce sont en général préparés et résolus. Préparés : la neuvième étant une note dissonante (c'est à une octave près la seconde de la fondamentale), l'accord qui précède doit contenir cette note, mais dans un accord consonant ; la neuvième est donc commune avec l'accord précédent. Résolus : la dissonance est résolue en abaissant la neuvième par un mouvement conjoint. Par exemple, en tonalité de ''do'' majeur, si l'on veut utiliser un accord de neuvième d'espèce sur la tonique ''(do - mi - sol - si - ré)'', on peut utiliser avant un accord de dominante ''(sol - si - ré)'' en préparation puis un accord parfait sur le degré {{Times New Roman|IV}} ''(fa - la - do)'' en résolution ; nous avons donc sur la voie la plus aigüe la succession ''ré'' (consonant) - ''ré'' (dissonant) - ''do'' (consonant).
On rencontre également parfois des accords de onzième et de treizième. On omet en général la tierce, car elle est dissonante avec la onzième. L'accord le plus fréquemment rencontré est l'accord sur la tonique : on considère alors que c'est un accord sur la dominante que l'on a enrichi « par le bas », en ajoutant une quinte inférieure. par exemple, dans la tonalité de ''do'' majeur, l'accord ''do - sol - si - ré - fa'' est considéré comme un accord de septième de dominante sur tonique, le degré étant noté « {{Times New Roman|V}}/{{Times New Roman|I}} ». De même pour l'accord ''do - sol - si - ré - fa - la'' qui est considéré comme un accord de neuvième de dominante sur tonique.
=== En jazz ===
En jazz, on utilise fréquemment l'accord de sixte à la place de l'accord de septième majeure sur la tonique. Par exemple, en ''do'' majeur, on utilise l'accord C<sup>6</sup> ''(do - mi - sol - la)'' à la place de C<sup>Δ</sup> ''(do - mi - sol - si)''. On peut noter que C<sup>6</sup> est un renversement de Am<sup>7</sup> et pourrait donc se noter Am<sup>7</sup>/C ; cependant, le fait de le noter C<sup>6</sup> indique que l'on a bien un accord sur la tonique qui s'inscrit dans la tonalité de ''do'' majeur (et non, par exemple, de ''la'' mineur naturelle) — par rapport à l'harmonie fonctionnelle, on remarquera que Am<sup>7</sup> a une fonction tonique, l'utilisation d'un renversement de Am<sup>7</sup> à la place d'un accord de C<sup>Δ</sup> est donc logique.
Les accords de neuvième, onzième et treizième sont utilisés comme accords de septième enrichis. Le chiffrage suit les règles habituelles : on ajoute un « 9 », un « 11 » ou un « 13 » au chiffrage de l'accord de septième.
On utilise également des accords dits « suspendus » : ce sont des accords de transition qui sont obtenus en prenant une triade majeure ou mineure et en remplaçant la tierce par la quarte juste (cas le plus fréquent) ou la seconde majeure. Plus particulièrement, lorsque l'on parle simplement « d'accord suspendu » sans plus de précision, cela désigne l'accord de neuvième avec une quarte suspendue, noté « 9sus4 » ou simplement « sus ».
== L'harmonie tonale ==
L'harmonie tonale est un ensemble de règle assez strictes qui s'appliquent dans la musique savante européenne, de la période baroque à la période classique classique ({{pc|xiv}}<sup>e</sup>-{{pc|xviii}}<sup>e</sup> siècle). Certaines règles sont encore largement appliquées dans divers styles musicaux actuels, y compris populaire (rock, rap…), d'autres sont au contraire ignorées (par exemple, un enchaînement de plusieurs accords de même qualité forme un mouvement parallèle, ce qui est proscrit en harmonie tonale). De nos jours, on peut voir ces règles comme des règles « de bon goût », et leur application stricte comme une manière de composer « à la manière de ».
Précédemment, nous avons vu la progression des accords. Ci-après, nous abordons aussi la manière dont les notes de l'accord sont réparties entre plusieurs voix, et comment on construit chaque voix.
=== Concepts fondamentaux ===
; Consonance
: Les intervalles sont considérés comme « plus ou moins consonants » :
:* consonance parfaite : unisson, quinte et octave ;
:* consonance mixte (parfaite dans certains contextes, imparfaite dans d'autres) : quarte ;
:* consonance imparfaite : tierce et sixte ;
:* dissonance : seconde et septième.
; Degrés
: Certains degrés sont considérés comme « forts », « meilleurs », ce sont les « notes tonales » : {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (sous-dominante) et {{Times New Roman|V}} (dominante).
[[Fichier:Mouvements harmoniques.svg|vignette|upright=0.75|Mouvements harmoniques.]]
; Mouvements
: Le mouvement décrit la manière dont les voix évoluent les unes par rapport aux autres :
:# Mouvement parallèle : les voix sont séparées par un intervalle constant.
:# Mouvement oblique : une voix reste constante, c'est le bourdon ; l'autre monte ou descend.
:# Mouvement contraire : une voix descend, l'autre monte.
:# Échange de voix : les voix échangent de note ; les mélodies se croisent mais on a toujours le même intervalle harmonique.
{{clear}}
=== Premières règles ===
; Règle du plus court chemin
: Quand on passe d'un accord à l'autre, la répartition des notes se fait de sorte que chaque voix fait le plus petit mouvement possible. Notamment : si les deux accords ont des notes en commun, alors les voix concernées gardent la même note.
: Les deux voix les plus importantes sont la voix aigüe — soprano — et la voix la plus grave — basse. Ces deux voix sont relativement libres : la voix de soprano a la mélodie, la voix de basse fonde l'harmonie. La règle du plus court chemin s'applique surtout aux voix intermédiaires ; si l'on a des mouvements conjoints, ou du moins de petits intervalles — c'est le sens de la règle du plus court chemin —, alors les voix sont plus faciles à interpréter. Cette règle évite également que les voix n'empiètent l'une sur l'autre (voir la règle « éviter le croisement des voix »).
; Éviter les consonances parfaites consécutives
:* Lorsque deux voix sont à l'unisson ou à l'octave, elles ne doivent pas garder le même intervalle, l'effet serait trop plat.
:* Lorsque deux voix sont à la quarte ou à la quinte, elles ne doivent pas garder le même intervalle, car l'effet est trop dur.
: Pour éviter cela, lorsque l'on part d'un intervalle juste, on a intérêt à pratiquer un mouvement contraire aux voix qui ne gardent pas la même note, ou au moins un mouvement direct : les voix vont dans le même sens, mais l'intervalle change.
: Notez que même avec le mouvement contraire, on peut avoir des consonances parfaites consécutives, par exemple si une voix fait ''do'' aigu ↗ ''sol'' aigu et l'autre ''sol'' médium ↘ ''do'' grave.
: L'interdiction des consonances parfaites consécutives n'a pas été toujours appliquée, le mouvement parallèle strict a d'ailleurs été le premier procédé utilisé dans la musique religieuse au {{pc|x}}<sup>e</sup> siècle. On peut par exemple utiliser des quintes parallèles pour donner un style médiéval au morceau. On peut également utiliser des octaves parallèles sur plusieurs notes afin de créer un effet de renforcement de la mélodie.
: Par ailleurs, les consonances parfaites consécutives sont acceptées lorsqu'il s'agit d'une cadence (transition entre deux parties ou bien conclusion du morceau).
; Éviter le croisement des voix
: Les voix sont organisées de la plus grave à la plus aigüe. Deux voix n'étant pas à l'unisson, celle qui est plus aigüe ne doit pas devenir la plus grave et ''vice versa''.
; Soigner la partie soprano
: Comme c'est celle qu'on entend le mieux, c'est en général celle qui porte la mélodie principale. On lui applique des règles spécifiques :
:# Si elle chante la sensible dans un accord de dominante ({{Times New Roman|V}}), alors elle doit monter à la tonique, c'est-à-dire que la note suivante sera la tonique située un demi-ton au dessus.
:# Si l'on arrive à une quinte ou une octave entre les parties basse et soprano par un mouvement direct, alors sur la partie soprano, le mouvement doit être conjoint. On doit donc arriver à cette situation par des notes voisines au soprano.
; Préférer certains accords
: Les deux degrés les plus importants sont la tonique ({{Times New Roman|I}}) et la dominante ({{Times New Roman|V}}), les accords correspondants ont donc une importance particulière.
: À l'inverse, l'accord de sensible ({{Times New Roman|VII}}) n'est pas considéré comme ayant une fonction harmonique forte. On le considère comme un accord de dominante affaibli. En tonalité mineure, on évite également l'accord de médiante ({{Times New Roman|III}}).
: Donc on utilise en priorité les accords de :
:# {{Times New Roman|I}} et {{Times New Roman|V}}.
:# Puis {{Times New Roman|II}}, {{Times New Roman|IV}}, {{Times New Roman|VI}} ; et {{Times New Roman|III}} en mode majeur.
:# On évite {{Times New Roman|VII}} ; et {{Times New Roman|III}} en mode mineur.
; Préférer certains enchaînements
: Les enchaînements d'accord peuvent être classés par ordre de préférence. Par ordre de préférence décroissante (du « meilleur » au « moins bon ») :
:# Meilleurs enchaînements : quarte ascendante ou descendante. Notons que la quarte est le renversement de la quinte, on a donc des enchaînements stables et naturels, mais avec un intervalle plus court qu'un enchaînement de quintes.
:# Bons enchaînements : tierce ascendante ou descendante. Les accords consécutifs ont deux notes en commun.
:# Enchaînements médiocres : seconde ascendante ou descendante. Les accords sont voisins, mais ils n'ont aucune note en commun. On les utilise de préférence en mouvement ascendant, et on utilise surtout les enchaînements {{Times New Roman|IV}}-{{Times New Roman|V}}, {{Times New Roman|V}}-{{Times New Roman|VI}} et éventuellement {{Times New Roman|I}}-{{Times New Roman|II}}.
:# Les autres enchaînements sont à éviter.
: On peut atténuer l'effet d'un enchaînement médiocre en plaçant le second accord sur un temps faible ou bien en passant par un accord intermédiaire.
[[Fichier:Progression Vplus4 I6.svg|thumb|Résolution d'un accord de triton (quarte sensible) vers l'accord de sixte de la tonique.]]
; La septième descend par mouvement conjoint
: Dans un accord de septième de dominante, la septième — qui est donc le degré {{Times New Roman|IV}} — descend par mouvement conjoint — elle est donc suivie du degré {{Times New Roman|III}}.
: Corolaire : un accord {{Times New Roman|V}}<sup>+4</sup> se résout par un accord {{Times New Roman|I}}<sup>6</sup> : on a bien un enchaînement {{Times New Roman|V}} → {{Times New Roman|I}}, et la 7{{e}} (degré {{Times New Roman|IV}}), qui est la basse de l'accord {{Times New Roman|V}}<sup>+4</sup>, descend d'un degré pour donner la basse de l'accord {{Times New Roman|I}}<sup>6</sup> (degré {{Times New Roman|III}}).
{{clear}}
[[Fichier:Progression I64 V7plus I5.svg|thumb|Accord de sixte et de quarte cadentiel.]]
; Un accord de sixte et quarte est un accord de passage
: Le second renversement d'un accord parfait est soit une appoggiature, soit un accord de passage, soit un accord de broderie.
: S'il s'agit de l'accord de tonique {{Times New Roman|I}}<sup>6</sup><sub>4</sub>, c'est « accord de sixte et quarte de cadence », l'appoggiature de l'accord de dominante de la cadence parfaite.
{{clear}}
Mais il faut appliquer ces règles avec discernement. Par exemple, la voix la plus aigüe est celle qui s'entend le mieux, c'est donc elle qui porte la mélodie principale. Il est important qu'elle reste la plus aigüe. La voix la plus grave porte l'harmonie, elle pose les accords, il est donc également important qu'elle reste la plus grave. Ceci a deux conséquences :
# Ces deux voix extrêmes peuvent avoir des intervalles mélodiques importants et donc déroger à la règle du plus court chemin : la voix aigüe parce que la mélodie prime, la voix de basse parce que la progression d'accords prime.
# Les croisements des voix intermédiaires sont moins critiques.
Par ailleurs, si l'on applique strictement toutes les règles « meilleurs accords, meilleurs enchaînements », on produit un effet conventionnel, stéréotypé. Il est donc important d'utiliser les solutions « moins bonnes », « médiocres » pour apporter de la variété.
Ajoutons que les renversements d'accords permettent d'avoir plus de souplesse : on reste sur le même accord, mais on enrichit la mélodie sur chaque voix.
Le ''Bolero'' de Maurice Ravel (1928) brise un certain nombre de ces règles. Par exemple, de la mesure 39 à la mesure 59, la harpe joue des secondes. De la mesure 149 à la mesure 165, les piccolo jouent à la sixte, dans des mouvement strictement parallèle, ce qui donne d'ailleurs une sonorité étrange. À partir de la mesure 239, de nombreux instruments jouent en mouvement parallèles (piccolos, flûtes, hautbois, cor, clarinettes et violons).
=== Application ===
[[Fichier:Harmonisation possible de frere jacques exercice.svg|vignette|Exercice : harmoniser ''Frère Jacques''.]]
Harmoniser ''Frère Jacques''.
Nous considérons un morceau à quatre voix : basse, ténor, alto et soprano. La soprano chante la mélodie de ''Frère Jacques''. L'exercice consiste à proposer l'écriture des trois autres voix en respectant les règles énoncées ci-dessus. Pour simplifier, nous ajoutons les contraintes suivantes :
* toutes les voix chantent des blanches ;
* nous nous limitons aux accords de quinte (accords de trois sons composés d'une tierce et d'une quinte) sans avoir recours à leurs renversements (accords de sixte, accords de sixte et de quarte).
Les notes à gauche de la portée indiquent la tessiture (ou ambitus), l'amplitude que peut chanter la voix.
{{clear}}
{{boîte déroulante/début|titre=Solution possible}}
[[Fichier:Harmonisation possible de frere jacques solution.svg|vignette|Harmonisation possible de ''Frère Jacques'' (solution de l'exercice).]]
Il n'y a pas qu'une solution possible.
Le premier accord doit contenir un ''do''. Nous sommes manifestement en tonalité de ''do'' majeur, nous proposons de commencer par l'accord parfait de ''do'' majeur, I<sup>5</sup>.
Le deuxième accord doit comporter un ''ré''. Si nous utilisons l'accord de quinte de ''ré'', nous allons créer une quinte parallèle. Nous pourrions utiliser un renversement, mais nous nous imposons de chercher un autre accord. Il peut s'agir de l'accord ''si''<sup>5</sup> ''(si-ré-fa)'' ou de l'accord de ''sol''<sup>5</sup> ''(sol-si-ré)''. La dernière solution permet d'utiliser l'accord de dominante qui est un accord important de la tonalité. La règle du plus court chemin imposerait le ''sol'' grave pour la partie de basse, mais cela est proche de la limite du chanteur, nous préférons passer au ''sol'' aigu, plus facile à chanter. Nous vérifions qu'il n'y a pas de quinte parallèle : l'intervalle ascendant ''do-sol'' (basse-alto) devient ''sol-si'' (3<sup>ce</sup>), l'intervalle descendant ''do-sol'' (soprano-alto) devient ''ré-si'' (3<sup>ce</sup>).
De la même manière, pour le troisième accord, nous ne pouvons pas passer à un accord de ''la''<sup>5</sup> pour éviter une quinte parallèle. Nous avons le choix entre ''do''<sup>5</sup> ''(do-mi-sol)'' et ''mi''<sup>5</sup> ''(mi-sol-si)''. Nous préférons revenir à l'accord de fondamental, solution très stable (l'enchaînement {{Times New Roman|V}}-{{Times New Roman|I}} formant une cadence parfaite).
Pour le quatrième accord, nous pourrions rester sur l'accord parfait de ''do'' mais cela planterait en quelque sorte la fin du morceau puisque l'on resterait sur la cadence parfaite ; or, nous connaissons le morceau et savons qu'il n'est pas fini. Nous choisissons l'accord de ''la''<sup>5</sup> qui est une sixte ascendante ({{Times New Roman|I}}-{{Times New Roman|VI}}).
Nos aurions pu répartir les voix différemment. Par exemple :
* alto : ''sol''-''si''-''sol''-''do'' ;
* ténor : ''mi''-''ré''-''mi''-''mi''.
{{boîte déroulante/fin}}
[[Fichier:Harmonisation possible de frere jacques.midi|vignette|Fichier son correspondant.]]
{{clear}}
== Annexe ==
=== Accords en musique classique ===
Un accord est un ensemble de notes jouées simultanément. Il peut s'agir :
* de notes jouées par plusieurs instruments ;
* de notes jouées par un même instrument : piano, clavecin, orgue, guitare, harpe (la plupart des instruments à clavier et des instruments à corde).
Pour deux notes jouées simultanément, on parle d'intervalle « harmonique » (par opposition à l'intervalle « mélodique » qui concerne les notes jouées successivement).
Les notes répétées à différentes octaves ne changent pas la nature de l'accord.
La musique classique considère en général des empilements de tierces ; un accord de trois notes sera constitué de deux tierces successives, un accord de quatre notes de trois tierces…
Lorsque tous les intervalles sont des intervalles impairs — tierces, quintes, septièmes, neuvièmes, onzièmes, treizièmes… — alors l'accord est dit « à l'état fondamental » (ou encore « primitif » ou « direct »). La note de la plus grave est appelée « fondamentale » de l'accord. Lorsque l'accord comporte un ou des intervalles pairs, l'accord est dit « renversé » ; la note la plus grave est appelée « basse ».
De manière plus générale, l'accord est dit à l'état fondamental lorsque la basse est aussi la fondamentale. On a donc un état idéal de l'accord (état canonique) — un empilement strict de tierces — et l'état réel de l'accord — l'empilement des notes réellement jouées, avec d'éventuels redoublements, omissions et inversions ; et seule la basse indique si l'accord est à l'état fondamental ou renversé.
Le chiffrage dit de « basse continue » ''({{lang|it|basso continuo}})'' désigne la représentation d'un accord sous la forme d'un ou plusieurs chiffres arabes et éventuellement d'un chiffre romain.
==== Accords de trois notes ====
En musique classique, les seuls accords considérés comme parfaitement consonants, c'est-à-dire sonnant agréablement à l'oreille, sont appelés « accords parfaits ». Si l'on prend une tonalité et un mode donné, alors l'accord construit par superposition es degrés I, III et V de cette gamme porte le nom de la gamme qui l'a généré.
[[fichier:Accord do majeur chiffre.svg|vignette|upright=0.5|Accord parfait de ''do'' majeur chiffré.]]
Par exemple :
* « l'accord parfait de ''do'' majeur » est composé des notes ''do'', ''mi'' et ''sol'' ;
* « l'accord parfait de ''la'' mineur » est composé des notes ''la'', ''do'' et ''mi''.
Un accord parfait majeur est donc composé, en partant de la fondamentale, d'une tierce majeure et d'une quinte juste. Un accord parfait mineur est composé d'une tierce mineure et d'une quinte juste.
L'accord parfait à l'état fondamental est appelé « accord de quinte » et est simplement chiffré « 5 » pour indiquer la quinte.
On peut également commencer un accord sur sa deuxième ou sa troisième note, en faisant monter celle(s) qui précède(nt) à l'octave suivante. On parle alors de « renversement d'accord » ou d'accord « renversé ».
[[Fichier:Accord do majeur renversements chiffre.svg|vignette|upright=0.75|Accord parfait de ''do'' majeur et ses renversements, chiffrés.]]
Par exemple,
* le premier renversement de l'accord parfait de ''do'' majeur est :<br /> ''mi'', ''sol'', ''do'' ;
* le second renversement de l'accord parfait de do majeur est :<br /> ''sol'', ''do'', ''mi''.
Les notes conservent leur nom de « fondamentale », « tierce » et « quinte » malgré le changement d'ordre. La note la plus grave est appelée « basse ».
Dans le cas du premier renversement, le deuxième note est la tierce de la basse (la note la plus grave) et la troisième note est la sixte ; le chiffrage en chiffres arabes est donc « 6 » (puisque l'on omet la tierce) et l'accord est appelé « accord de sixte ». Pour le deuxième renversement, les intervalles sont la quarte et la sixte, le chiffrage est donc « 6-4 » et l'accord est appelé « accord de sixte et de quarte ».
Dans tous les cas, on chiffre le degré on considérant la fondamentale, par exemple {{Times New Roman|I}} si l'accord est construit sur la tonique de la gamme.
Les autres accords de trois notes que l'on rencontre sont :
* l'accord de quinte diminuée, constitué d'une tierce mineure et d'une quinte diminuée ; lorsqu'il est construit sur le septième degré d'une gamme, on considère que c'est un accord de septième de dominante sans fondamentale (voir plus bas), le degré est donc indiqué « “{{Times New Roman|V}}” » (cinq entre guillemets) et non « {{Times New Roman|VII}} » ;
* l'accord de quinte augmenté : il est composé d'une tierce majeure et qu'une quinte augmentée.
Dans le tableau ci-dessous,
* « m » désigne un intervalle mineur ;
* « M » un intervalle majeur ou le mode majeur ;
* « J » un intervalle juste ;
* « d » un intervalle diminué ;
* « A » un intervalle augmenté ;
* « mh » le mode mineur harmonique ;
* « ma » le mode mineur ascendant ;
* « md » le mode mineur descendant.
{| class="wikitable"
|+ Accords de trois notes
! scope="col" rowspan="2" | Nom
! scope="col" rowspan="2" | 3<sup>ce</sup>
! scope="col" rowspan="2" | 5<sup>te</sup>
! scope="col" rowspan="2" | État fondamental
! scope="col" rowspan="2" | 1<sup>er</sup> renversement
! scope="col" rowspan="2" | 2<sup>nd</sup> renversement
! scope="col" colspan="4"| Construit sur les degrés
|-
! scope="col" | M
! scope="col" | mh
! scope="col" | ma
! scope="col" | md
|-
| Accord parfait<br /> majeur || M || J
| accord de quinte || accord de sixte || accord de<br />sixte et de quarte
| {{Times New Roman|I, IV, V}} || {{Times New Roman|V, VI}} || {{Times New Roman|IV, V}} || {{Times New Roman|III, VI, VII}}
|-
| Accord parfait<br /> mineur || m || J
| accord de quinte || accord de sixte || accord de<br />sixte et de quarte
| {{Times New Roman|II, III, VI}} || {{Times New Roman|I, IV}} || {{Times New Roman|I, II}} || {{Times New Roman|I, IV, V}}
|-
| Accord de<br />quinte diminuée || m || d
| accord de<br />quinte diminuée || accord de<br />sixte sensible<br />sans fondamentale || accord de triton<br />sans fondamentale
| {{Times New Roman|VII (“V”)}} || {{Times New Roman|II, VII (“V”)}} || {{Times New Roman|VI, VII (“V”)}} || {{Times New Roman|II}}
|-
| Accord de<br />quinte augmentée || M || A
| accord de<br />quinte augmentée || accord de sixte<br />et de tierce sensible || accord de sixte et de quarte<br />sur sensible
| || {{Times New Roman|III}} || {{Times New Roman|III}} ||
|}
==== Accords de quatre notes ====
Les accords de quatre notes sont des accord composés de trois tierces superposées. La dernière note étant le septième degré de la gamme, on parle aussi d'accords de septième.
Ces accords sont dissonants : ils contiennent un intervalle de septième (soit une octave montante suivie d'une seconde descendante). Ils laissent donc une impression de « tension ».
Il existe sept différents types d'accords, ou « espèces ». Citons l'accord de septième de dominante, l'accord de septième mineure et l'accord de septième majeure.
===== L'accord de septième de dominante =====
[[Fichier:Accord 7e dominante do majeur renversements chiffre.svg|vignette|Accord de septième de dominante de ''do'' majeur et ses renversements, chiffrés.]]
L'accord de septième de dominante est l'empilement de trois tierces à partir de la dominante de la gamme, c'est-à-dire du {{Times New Roman|V}}<sup>e</sup> degré. Par exemple, l'accord de septième de dominante de ''do'' majeur est l'accord ''sol''-''si''-''ré''-''fa'', et l'accord de septième de dominante de ''la'' mineur est ''mi''-''sol''♯-''si''-''ré''. L'accord de septième de dominante dont la fondamentale est ''do'' (''do''-''mi''-''sol''-''si''♭) appartient à la gamme de ''fa'' majeur.
Que le mode soit majeur ou mineur, il est composé d'une tierce majeure, d'une quinte juste et d'une septième mineure (c'est un accord parfait majeur auquel on ajoute une septième mineure). C'est de loin l'accord de septième le plus utilisé ; il apparaît au {{pc|xvii}}<sup>e</sup> en musique classique.
Dans son état fondamental, son chiffrage est {{Times New Roman|V 7/+}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}). Le signe plus indique la sensible.
Son premier renversement est appelé « accord de quinte diminuée et sixte » et est noté {{Times New Roman|V 6/<s>5</s>}} (ou {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}).
Son deuxième renversement est appelé « accord de sixte sensible », puisque la sixte de l'accord est la sensible de la gamme, et est noté {{Times New Roman|V +6}} (ou {{Times New Roman|V<sup>+6</sup>}}).
Son troisième renversement est appelé « accord de quarte sensible » et est noté {{Times New Roman|V +4}} (ou {{Times New Roman|V<sup>+4</sup>}}).
[[Fichier:Accord 7e dominante sans fondamentale do majeur renversements chiffre.svg|vignette|Accord de septième de dominante sans fondamentale de ''do'' majeur et ses renversements, chiffrés.]]
On utilise aussi l'accord de septième de dominante sans fondamentale ; c'est alors un accord de trois notes.
Dans son état fondamental, c'est un « accord de quinte diminuée » placé sur le {{Times New Roman|VII}}<sup>e</sup> degré (mais c'est bien un accord construit sur le {{Times New Roman|V}}<sup>e</sup> degré), noté {{Times New Roman|“V” <s>5</s>}} (ou {{Times New Roman|“V”<sup><s>5</s></sup>}}). Notez les guillemets qui indiquent que la fondamentale V est absente.
Dans son premier renversement, c'est un « accord de sixte sensible sans fondamentale » noté {{Times New Roman|“V” +6/3}} (ou {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}}).
Dans son second renversement, c'est un « accord de triton sans fondamentale » (puisque le premier intervalle est une quarte augmentée qui comporte trois tons) noté {{Times New Roman|“V” 6/+4}} (ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}).
Notons qu'un accord de septième de dominante n'a pas toujours la dominante pour fondamentale : tout accord composé d'une tierce majeure, d'une quinte juste et d'une septième mineure est un accord de septième de dominante et est chiffré {{Times New Roman|<sup>7</sup><sub>+</sub>}}, quel que soit le degré sur lequel il est bâti (certaines notes peuvent avoir une altération accidentelle).
===== Les accords de septième d'espèce =====
Les autres accords de septièmes sont dits « d'espèce ».
L'accord de septième mineure est l'accord de septième formé sur la fondamentale d'une gamme mineure ''naturelle''. Par exemple, l'accord de septième mineure de ''la'' est ''la''-''do''-''mi''-''sol''. Il est composé d'une tierce mineure, d'une quinte juste et d'une septième mineure (c'est un accord parfait mineur auquel on ajoute une septième mineure).
L'accord de septième majeure est l'accord de septième formé sur la fondamentale d'une gamme majeure. Par exemple, L'accord de septième majeure de ''do'' est ''do''-''mi''-''sol''-''si''. Il est composé d'une tierce majeure, d'une quinte juste et d'une septième majeure (c'est un accord parfait majeur auquel on ajoute une septième majeure).
==== Utilisation du chiffrage ====
Le chiffrage est utilisé de deux manières.
La première manière, c'est la notation de la basse continue. La basse continue est une technique d'improvisation utilisée dans le baroque pour l'accompagnement d'instruments solistes. Sur la partition, on indique en général la note de basse de l'accord et le chiffrage en chiffres arabes.
La seconde manière, c'est pour l'analyse d'une partition. Le fait de chiffrer les accords permet de mieux en comprendre la structure.
De manière générale, on peut retenir que :
* le chiffrage « 5 » indique un accord parfait, superposition d'une tierce (majeure ou mineure) et d'une quinte juste ;
* le chiffrage « 6 » indique le premier renversement d'un accord parfait ;
* le chiffrage « 6/4 » indique le second renversement d'un accord parfait ;
* chiffrage « 7/+ » indique un accord de septième de dominante ;
* le signe « + » indique en général que la note de l'intervalle est la sensible ;
* un intervalle barré désigne un intervalle diminué.
[[fichier:Accords gamme do majeur la mineur.svg|class=transparent| center | Principaux accords construits sur les gammes de ''do'' majeur et de ''la'' mineur harmonique.]]
=== Notation « jazz » ===
En jazz et de manière générale en musique rock et populaire, la base d'un accord est la triade composée d'une tierce (majeure ou mineure) et d'une quinte juste. Pour désigner un accord, on utilise la note fondamentale, éventuellement désigné par une lettre dans le système anglo-saxon (A pour ''la'' etc.), suivi d'une qualité (comme « m », « + »…).
Les renversements ne sont pas notés de manière particulière, ils sont notés comme les formes fondamentales.
Dans les deux tableaux suivants, la fondamentale est notée X (remplace le C pour un accord de ''do'', le D pour un accord de ''ré''…). La construction des accords est décrite par la suite.
[[Fichier:Arbre accords triades 5d5J5A.svg|vignette|upright=1.5|Formation des triades présentée sous forme d'arbre.]]
{| class="wikitable"
|+ Notation des principales triades
|-
|
! scope="col" | Tierce<br />mineure (3m)
! scope="col" | Tierce<br />majeure (3M)
|-
! scope="row" | Quinte diminuée (5d)
| X<sup>o</sup>, Xm<sup>♭5</sup>, X–<sup>♭5</sup> ||
|-
! scope="row" | Quinte juste (5J)
| Xm, X– || X
|-
! scope="row" | Quinte augmentée (5A)
| || X+, X<sup>♯5</sup>
|}
[[Fichier:Triades do.svg|class=transparent|center|Triades de do.]]
{| class="wikitable"
|+ Notation des principaux accords de septième
|-
| colspan="2" |
! scope="col" | Tierce<br />mineure (3m)
! scope="col" | Tierce<br />majeure (3M)
|-
! scope="row" rowspan="2" | Quinte<br />diminuée (5d)
! scope="row" | Septième diminuée (7d)
| X<sup>o7</sup> ||
|-
! scope="row" | Septième mineure (7m)
| Xm<sup>7(♭5)</sup>, X–<sup>7(♭5)</sup>, X<sup>Ø</sup> ||
|-
! scope="row" rowspan="3" | Quinte<br />juste (5J)
! scope="row" | Sixte majeure (6M)
| Xm<sup>6</sup> || X<sup>6</sup>
|-
! scope="row" | Septième mineure (7m)
| Xm<sup>7</sup>, X–<sup>7</sup> || X<sup>7</sup>
|-
! scope="row" | Septième majeure (7M)
| Xm<sup>maj7</sup>, X–<sup>maj7</sup>, Xm<sup>Δ</sup>, X–<sup>Δ</sup> || X<sup>maj7</sup>, X<sup>Δ</sup>
|-
! scope="row" rowspan="2" | Quinte<br />augmentée (5A)
! scope="row" | Septième mineure (7m)
| || X+<sup>7</sup>
|-
! scope="row" | Septième majeure (7M)
| || X+<sup>maj7</sup>
|}
[[Fichier:Arbre accords septieme.svg|class=transparent|center|Formation des accords de septième présentée sous forme d'arbre.]]
[[Fichier:Accords do septieme.svg|class=transparent|center|Accord de do septième.]]
On notera que l'intervalle de sixte majeure est l'enharmonique de celui de septième diminuée (6M = 7d).
[[File:Principaux accords do.svg|class=transparent|center|Principaux accords de do.]]
==== Triades ====
; Accords fondés sur une tierce majeure
* accord parfait majeur : pas de notation
*: p. ex. « ''do'' » ou « C » pour l'accord parfait de ''do'' majeur (''do'' - ''mi'' - ''sol'')
; Accords fondés sur une tierce mineure
* accord parfait mineur : « m », « min » ou « – »
*: « ''do'' m », « ''do'' – », « Cm », « C– »… pour l'accord parfait de ''do'' mineur (''do'' - ''mi''♭ - ''sol'')
==== Triades modifiées ====
; Accords fondés sur une tierce majeure
* accord augmenté (la quinte est augmentée) : aug, +, ♯5
*: « ''do'' aug », « ''do'' + », « ''do''<sup>♯5</sup> » « Caug », « C+ » ou « C<sup>♯5</sup> » pour l'accord de ''do'' augmenté (''do'' - ''mi'' - ''sol''♯)
: L'accord augmenté est un empilement de tierces majeures. Ainsi, un accord augmenté a deux notes communes avec deux autres accords augmentés : C+ (''do'' - ''mi'' - ''sol''♯) a deux notes communes avec A♭+ (''la''♭ - ''do'' - ''mi'') et avec E+ (''mi'' - ''sol''♯ - ''si''♯) ; et on remarque que ces trois accords sont en fait enharmoniques (avec les enharmonies ''la''♭ = ''sol''♯ et ''si''♯ = ''do''). En effet, l'octave comporte six tons (sous la forme de cinq tons et deux demi-tons), et une tierce majeure comporte deux tons, on arrive donc à l'octave en ajoutant une tierce majeure à la dernière note de l'accord.
; Accords fondés sur une tierce mineure
* accord diminué (la quinte est diminuée) : dim, o, ♭5
*: « ''do'' dim », « ''do''<sup>o</sup> », « ''do''<sup>♭5</sup> », « Cdim », « C<sup>o</sup> » ou « C<sup>♭5</sup> » pour l'accord de ''do'' diminuné (''do'' - ''mi''♭ - ''sol''♭)
: On remarque que la quinte diminuée est l'enharmonique de la quarte augmentée et est l'intervalle appelé « triton » (car composé de trois tons).
; Accords fondés sur une tierce majeure ou mineure
* accord suspendu de seconde : la tierce est remplacée par une seconde majeure : sus2
*: « ''do''<sup>sus2</sup> » ou « C<sup>sus2</sup> » pour l'accord de ''do'' majeur suspendu de seconde (''do''-''ré''-''sol'')
* accord suspendu de quarte : la tierce est remplacée par une quarte juste : sus4
*: « ''do''<sup>sus4</sup> » ou « C<sup>sus4</sup> » pour l'accord de ''do'' majeur suspendu de quarte (''do''-''fa''-''sol'')
==== Triades appauvries ====
; Accords fondés sur une tierce majeure ou mineure
* accord de puissance : la tierce est omise, l'accord n'est constitué que de la fondamentale et de la quinte juste : 5
*: « ''do''<sup>5</sup> », « C<sup>5</sup> » pour l'accord de puissance de ''do'' (''do'' - ''la'')
{{note|Très utilisé dans les musiques rock, hard rock et heavy metal, il est souvent joué renversé (''la'' - ''do'') ou bien avec l'ajout de l'octave (''do'' - ''la'' - ''do'').}}
==== Triades enrichies ====
; Accords fondés sur une tierce majeure
* accord de septième (la 7<sup>e</sup> est mineure) : 7
*: « ''do''<sup>7</sup> », « C<sup>7</sup> » pour l'accord de ''do'' septième, appelé « accord de septième de dominante de ''fa'' majeur » en musique classique (''do'' - ''mi'' - ''sol'' - ''si''♭)
* accord de septième majeure : Δ, 7M ou maj7
*: « ''do'' <sup>Δ</sup> », « ''do'' <sup>maj7</sup> », « C<sup>Δ</sup> », « C<sup>7M</sup> »… pour l'accord de ''do'' septième majeure (''do'' - ''mi'' - ''sol'' - ''si'')
; Accords fondés sur une tierce mineure
* accord de mineur septième (la tierce et la 7<sup>e</sup> sont mineures) : m7, min7 ou –7
*: « ''do'' m<sup>7</sup> », « ''do'' –<sup>7</sup> », « Cm<sup>7</sup> », « C–<sup>7</sup> »… pour l'accord de ''do'' mineur septième, appelé « accord de septième de dominante de ''fa'' mineur » en musique classique (''do'' - ''mi''♭ - ''sol'' - ''si''♭)
* accord mineure septième majeure : m7M, m7maj, mΔ, –7M, –7maj, –Δ
*: « ''do'' m<sup>7M</sup> », « ''do'' m<sup>maj7</sup> », « ''do'' –<sup>Δ</sup> », « Cm<sup>7M</sup> », « Cm<sup>maj7</sup> », « C–<sup>Δ</sup> »… pour l'accord de ''do'' mineur septième majeure (''do'' - ''mi''♭ - ''sol'' - ''si'')
* accord de septième diminué (la quinte et la septième sont diminuée) : dim 7 ou o7
*: « ''do'' dim<sup>7</sup> », « ''do''<sup>o7</sup> », « Cdim<sup>7</sup> » ou « C<sup>o7</sup> » pour l'accord de ''do'' septième diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si''♭)
* accord demi-diminué (seule la quinte est diminuée, la septième est mineure) : Ø ou –7(♭5)
*: « ''do''<sup>Ø</sup> », « ''do''<sup>7(♭5)</sup> », « C<sup>Ø</sup> » ou « C<sup>7♭5</sup> » pour l'accord de ''do'' demi-diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si'')
=== Construction pythagoricienne des accords ===
Nous avons vu au débuts que lorsque l'on joue deux notes en même temps, leurs vibrations se superposent. Certaines superpositions créent un phénomène de battement désagréable, c'est le cas des secondes.
Dans le cas d'une tierce majeure, les fréquences des notes quadruple et quintuple d'une même base : les fréquences s'écrivent 4׃<sub>0</sub> et 5׃<sub>0</sub>. Cette superposition de vibrations est agréable à l'oreille. Nous avons également vu que dans le cas d'une quinte juste, les fréquences sont le double et le triple d'une même base, ou encore le quadruple et sextuple si l'on considère la moitié de cette base.
Ainsi, dans un accord parfait majeur, les fréquences des fondamentales des notes sont dans un rapport 4, 5, 6. De même, dans le cas d'un accord parfait mineur, les proportions sont de 1/6, 1/5 et 1/4.
{{voir|[[../Caractéristiques_et_notation_des_sons_musicaux#Construction_pythagoricienne_et_gamme_de_sept_tons|Caractéristiques et notation des sons musicaux > Construction pythagoricienne et gamme de sept tons]]}}
=== Un peu de physique : interférences ===
Les sons sont des vibrations. Lorsque l'on émet deux sons ou plus simultanément, les vibrations se superposent, on parle en physique « d'interférences ».
Le modèle le plus simple pour décrire une vibration est la [[w:fr:Fonction sinus|fonction sinus]] : la pression de l'air P varie en fonction du temps ''t'' (en secondes, s), et l'on a pour un son « pur » :
: P(''t'') ≈ sin(2π⋅ƒ⋅''t'')
où ƒ est la fréquence (en hertz, Hz) du son.
Si l'on émet deux sons de fréquence respective ƒ<sub>1</sub> et ƒ<sub>2</sub>, alors la pression vaut :
: P(''t'') ≈ sin(2π⋅ƒ<sub>1</sub>⋅''t'') + sin(2π⋅ƒ<sub>2</sub>⋅''t'').
Nous avons ici une [[w:fr:Identité trigonométrique#Transformation_de_sommes_en_produits,_ou_antilinéarisation|identité trigonométrique]] dite « antilinéarisation » :
: <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{f_1 + f_2}{2}t \right ) \cdot \sin \left ( 2\pi \frac{f_1 - f_2}{2}t \right ).</math>
On peut étudier simplement deux situations simples.
[[Fichier:Battements interferentiels.png|vignette|Deux sons de fréquences proches créent des battements : la superposition d'une fréquence et d'une enveloppe.]]
La première, c'est quand les fréquences ƒ<sub>1</sub> et ƒ<sub>2</sub> sont très proches. Alors, la moyenne (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2 est très proche de ƒ<sub>1</sub> et ƒ<sub>2</sub> ; et la demie différence (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2 est très proche de zéro. On a donc une enveloppe de fréquence très faible, (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2, dans laquelle s'inscrit un son de fréquence moyenne, (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2. C'est cette enveloppe de fréquence très faible qui crée les battements, désagréables à l'oreille.
Sur l'image ci-contre, le premier trait rouge montre un instant où les vibrations sont opposées ; elles s'annulent, le son s'éteint. Le second trait rouge montre un instant où les vibrations sont en phase : elle s'ajoutent, le son est au plus fort.
{{clear}}
La seconde, c'est lorsque les deux fréquences sont des multiples entiers d'une même fréquence fondamentale ƒ<sub>0</sub> : ƒ<sub>1</sub> = ''n''<sub>1</sub>⋅ƒ<sub>0</sub> et ƒ<sub>0</sub> = ''n''<sub>0</sub>⋅ƒ<sub>0</sub>. On a alors :
: <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{n_1 + n_2}{2}f_0 \cdot t \right ) \cdot \sin \left ( 2\pi \frac{n_1 - n_2}{2}f_0 \cdot t \right ).</math>
On multiplie donc deux fonctions qui ont des fréquences multiples de ƒ<sub>0</sub>. La différence minimale entre ''n''<sub>1</sub> et ''n''<sub>2</sub> vaut 1 ; on a donc une enveloppe dont la fréquence est au minimum la moitié de ƒ<sub>0</sub>, c'est-à-dire un son une octave en dessous de ƒ<sub>0</sub>. Donc, cette enveloppe ne crée pas d'effet de battement, ou plutôt, le battement est trop rapide pour être perçu comme tel. Dans cette enveloppe, on a une fonction sinus dont la fréquence est également un multiple de ƒ<sub>0</sub> ; l'enveloppe et la fonction qui y est inscrite ont donc de nombreux « points communs », d'où l'effet harmonieux.
=== Le tonnetz ===
[[File:Speculum musicae.png|thumb|right|225px|Euler, ''De harmoniæ veris principiis'', 1774, p. 350.]]
En allemand, le terme ''Tonnetz'' (se prononce « tône-netz ») signifie « réseau tonal ». C'est une représentation graphique des notes qui a été imaginée par [[w:Leonhard Euler|Leonhard Euler]] en 1739.
Cette représentation graphique peut aider à la mémorisation de certains concepts de l'harmonie. Cependant, son application est très limitée : elle ne concerne que l'intonation juste d'une part, et que les accords parfait des tonalités majeures et mineures naturelles d'autre part. La représentation contenant les douze notes de la musique savante occidentale, on peut bien sûr représenter d'autres objets, comme les accords de septième ou les accords diminués, mais la représentation graphique est alors compliquée et perd son intérêt pédagogique.
On part d'une note, par exemple le ''do''. Si on progresse vers la droite, on monte d'une quinte juste, donc ''sol'' ; vers la gauche, on descend d'une quinte juste, donc ''fa''. Si on va vers le bas, on monte d'une tierce majeure, donc ''mi'' ; si on va vers le haut, on descend d'une tierce majeure, donc ''la''♭ ou ''sol''♯
fa — do — sol — ré
| | | |
la — mi — si — fa♯
| | | |
do♯ — sol♯ — ré♯ — si♭
La figure forme donc un filet, un réseau. On voit que ce réseau « boucle » : si on descend depuis le ''do''♯, on monte d'une tierce majeure, on obtient un ''mi''♯ qui est l'enharmonique du ''fa'' qui est en haut de la colonne. Si on va vers la droite à partir du ''ré'', on obtient le ''la'' qui est au début de la ligne suivante.
Si on ajoute des diagonales allant vers la droite et le haut « / », on met en évidence des tierces mineures : ''la'' - ''do'', ''mi'' - ''sol'', ''si'' - ''ré'', ''do''♯ - ''mi''…
fa — do — sol — ré
| / | / | / |
la — mi — si — fa♯
| / | / | / |
do♯ — sol♯ — ré♯ — si♭
Donc les liens représentent :
* | : tierce majeure ;
* — : quinte juste ;
* / : tierce mineure.
[[Fichier:Tonnetz carre accords fr.svg|thumb|Tonnetz avec les accords parfaits. Les notes sont en notation italienne et les accords en notation jazz.]]
On met ainsi en évidence des triangles dont un côté est une quinte juste, un côté une tierce majeure et un côté une tierce mineure ; c'est-à-dire que les notes aux sommets du triangle forment un accord parfait majeur (par exemple ''do'' - ''mi'' - ''sol'') :
<div style="font-family:courier; background-color:#fafafa">
fa — '''do — sol''' — ré<br />
| / '''| /''' | / |<br />
la — '''mi''' — si — fa♯<br />
| / | / | / |<br />
do♯ — sol♯ — ré♯ — si♭
</div>
ou un accord parfait mineur (''la'' - ''do'' - ''mi'').
<div style="font-family:courier; background-color:#fafafa">
fa — '''do''' — sol — ré<br />
| '''/ |''' / | / |<br />
'''la — mi''' — si — fa♯<br />
| / | / | / |<br />
do♯ — sol♯ — ré♯ — si♭
</div>
Un triangle représente donc un accord, et un sommet représente une note. Si on passe d'un triangle à un triangle voisin, alors on passe d'un accord à un autre accord, les deux accords ayant deux notes en commun. Ceci illustre la notion de « plus court chemin » en harmonie : si on passe d'un accord à un autre en gardant un côté commun, alors on a un mouvement conjoint sur une seule des trois voix.
Par rapport à l'harmonie fonctionnelle : les accords sont contigus à leur fonction, par exemple en ''do'' majeur :
* fonction de tonique ({{Times New Roman|I}}) : C, A– et E– sont contigus ;
* fonction de sous-dominante ({{Times New Roman|IV}}) : F et D– sont contigus ;
* fonction de dominante ({{Times New Roman|V}}) : G et B<sup>o</sup> sont contigus.
On notera que les triangles d'un schéma ''tonnetz'' ne représentent que des accords parfaits. Pour représenter un accord de quinte diminuée (''si'' - ''ré'' - ''fa'') ou les accords de septième, en particulier l'accord de septième de dominante, il faut étendre le ''tonnetz'' et l'on obtient des figures différentes. Par ailleurs, il est adapté à ce que l'on appelle « l'intonation juste », puisque tous les intervalles sont idéaux.
[[Fichier:Tonnetz carre accords etendu fr.svg|vignette|Tonnetz étendu.]]
[[Fichier:Tonnetz carre do majeur accords fr.svg|vignette|Tonnetz de la tonalité de ''do'' majeur. La représentation de l'accord de quinte diminuée sur ''si'' (B<sup>o</sup>) est une ligne et non un triangle.]]
[[Fichier:Tonnetz carre do mineur accords fr.svg|vignette|Tonnetz des tonalités de ''do'' mineur naturel (haut) et ''do'' mineur harmonique (bas).]]
Si l'on étend un peu le réseau :
ré♭ — la♭ — mi♭ — si♭ — fa
| / | / | / | / |
fa — do — sol — ré — la
| / | / | / | / |
la — mi — si — fa♯ — do♯
| / | / | / | / |
do♯ — sol♯ — ré♯ — la♯ — mi♯
| / | / | / | / |
mi♯ — do — sol — ré — la
on peut donc trouver des chemins permettant de représenter les accords de septième de dominante (par exemple en ''do'' majeur, G<sup>7</sup>)
fa
/
sol — ré
| /
si
et des accords de quinte diminuée (en ''do'' majeur : B<sup>o</sup>)
fa
/
ré
/
si
Une gamme majeure ou mineure naturelle peut se représenter par un trapèze rectangle : ''do'' majeur
fa — do — sol — ré
| /
la — mi — si
et ''do'' mineur
la♭ — mi♭ — si♭
/ |
fa — do — sol — ré
En revanche, la représentation d'une tonalité nécessite d'étendre le réseau afin de pouvoir faire figurer tous les accords, deux notes sont représentées deux fois. La représentation des tonalités mineures harmoniques prend une forme biscornue, ce qui nuit à l'intérêt pédagogique de la représentation.
[[Fichier:Neo-Riemannian Tonnetz.svg|vignette|upright=2|Tonnetz avec des triangles équilatéraux.]]
On peut réorganiser le schéma en décalant les lignes, afin d'avoir des triangles équilatéraux. Sur la figure ci-contre (en notation anglo-saxonne) :
* si on monte en allant vers la droite « / », on a une tierce mineure ;
* si on descend en allant vers la droite « \ », on a une tierce majeure ;
* les liens horizontaux « — » représentent toujours des quintes justes
* les triangles pointe en haut sont des accords parfaits mineurs ;
* les triangles pointe en bas sont des accords parfaits majeurs.
On a alors les accords de septième de dominante
F
/
G — D
\ /
B
et de quinte diminuée
F
/
D
/
B
les tonalités majeures
F — C — G — D
\ /
A — E — B
et les tonalités mineures naturelles
A♭ — E♭ — B♭
/ \
F — C — G — D
== Notes et références ==
{{références}}
== Voir aussi ==
=== Liens externes ===
{{wikipédia|Consonance (harmonie tonale)}}
{{wikipédia|Disposition de l'accord}}
{{wikisource|Petit Manuel d’harmonie}}
* {{lien web
| url = https://www.apprendrelesolfege.com/chiffrage-d-accords
| titre = Chiffrage d'accords (classique)
| site = Apprendrelesolfege.com
| consulté le = 2020-12-03
}}
* {{lien web
| url = https://www.coursd-harmonie.fr/introduction/introduction2_le_chiffrage_d_accords.php
| titre = Introduction II : Le chiffrage d'accords
| site = Cours d'harmonie.fr
| consulté le = 2021-12-14
}}
* {{lien web
| url=https://www.coursd-harmonie.fr/
| titre = Cours d'harmonie en ligne
| auteur = Jean-Baptiste Voinet
| site=coursd-harmonie.fr
| consulté le = 2021-12-20
}}
* {{lien web
| url=http://e-harmonie.e-monsite.com/
| titre = Cours d'harmonie classique en ligne
| auteur = Olivier Miquel
| site=e-harmonie
| consulté le = 2021-12-24
}}
* {{lien web
| url=https://fr.audiofanzine.com/theorie-musicale/editorial/dossiers/les-gammes-et-les-modes.html
| titre = Les bases de l’harmonie
| site = AudioFanzine
| date = 2013-07-23
| consulté le = 2024-01-12
}}
----
''[[../Mélodie|Mélodie]]'' < [[../|↑]] > ''[[../Représentation musicale|Représentation musicale]]''
[[Catégorie:Formation musicale (livre)|Harmonie]]
kckkveu9o936ff4iyha75ej8hg3a9yf
745863
745862
2025-07-03T11:14:44Z
Cdang
1202
/* Principaux accords */ simplification
745863
wikitext
text/x-wiki
{{Bases de solfège}}
<span style="font-size:25px;">6. Harmonie</span>
L'harmonie désigne les notes jouées en même temps, soit plusieurs instruments jouant chacun une note, soit un instrument jouant un accord (instrument dit polyphonique).
== Première approche ==
L'exemple le plus simple d'harmonie est sans doute la chanson en canon : c'est un chant polyphonique, c'est-à-dire à plusieurs voix, chaque voix chantant la même chose en décalé. Prenons par exemple ''Vent frais, vent du matin'' (la version originale est ''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609) :
[[Fichier:Vent frais vent du matin.svg|class=transparent|center|Partition de ''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]]
[[Fichier:Vent frais vent du matin.midi|vignette|''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]]
nous voyons que les voix se superposent de manière « harmonieuse ». Les notes de chaque voix se correspondent point par point (avec un retard), c'est donc un type d'harmonie polyphonique appelé « contrepoint ».
Considérons la première note de la mesure 6 pour chaque voix. Nous avons la superposition des notes ''ré''-''fa''-''la'' (du grave vers l'aigu) ; la superposition de notes jouées ou chantées ensembles s'appelle un accord. Cet accord ''ré''-''fa''-''la'' porte le nom « d'accord parfait de ''ré'' mineur » :
* « ''ré'' » car la note fondamentale est un ''ré'' ;
* « parfait » car il est l'association d'une tierce, ''ré''-''fa'', et d'une quinte juste, ''ré''-''la'' ;
* « mineur » car le premier intervalle, ''ré''-''fa'', est une tierce mineure.
Considérons maintenant un chant accompagné au piano. La piano peut jouer plusieurs notes en même temps, il peut jouer des accords.
[[Fichier:Au clair de le lune chant et piano.svg|class=transparent|center|Deux premières mesure d’Au clair de la lune.]]
[[Fichier:Au clair de le lune chant et piano.midi|vignette|Deux premières mesure d’Au clair de la lune.]]
L'accord, les notes à jouer simultanément, sont écrites « en colonne ». Lorsqu'on les énonce, on les lit de bas en haut mais le pianiste les joue en pressant les touches du clavier en même temps, de manière « plaquée ».
Le premier accord est composé des notes ''do''-''mi''-''sol'' ; il est appelé « accord parfait de ''do'' majeur » car la note fondamentale est ''do'', qu'il est l'association d'une tierce et d'une quinte juste et que le premier intervalle, ''do''-''mi'', est une tierce majeure.
== Consonance et dissonance ==
Les notions de consonance et de dissonance sont culturelles et changent selon l'époque. Nous pouvons néanmoins noter que :
* l'accord de seconde, et son renversement la septième, créent des battements, les notes « frottent », c'est un intervalle harmonique dissonant ; mais dans le cas de la septième, comme les notes sont éloignées, le frottement est moins perceptible ;
* les accords de tierce, quarte et quinte sonnent agréablement à l'oreille, ils sont consonants.
Dans la musique savante européenne, au début au du Moyen-Âge, seuls les accords de quarte et de quinte étaient considérés comme consonants, d'où leur qualification de « juste ». La tierce, et son renversement la sixte, étaient perçues comme dissonantes.
L'harmonie joue avec les consonances et les dissonances. Dans un premier temps, les harmonies dissonantes sont utilisées pour créer des tensions qui sont ensuite résolues, on utilise des successions « consonant-dissonant-consonant ». À force d'entendre des intervalles considérés comme dissonants, l'oreille s'habitue et certains finissent par être considérés comme consonants ; c'est ce qui est arrivé à la tierce et à la sixte à la fin du Moyen Âge avec le contrepoint.
Il faut ici aborder la notion d'harmonique des notes.
[[File:Harmoniques de do.svg|thumb|Les six premières harmoniques de ''do''.]]
Lorsque l'on joue une note, on entend d'autres notes plus aigües et plus faibles ; la note jouée est appelée la « fondamentale » et les notes plus aigües et plus faibles sont les « harmoniques ». C'est cette accumulation d'harmoniques qui donne la couleur au son, son timbre, qui fait qu'un piano ne sonne pas comme un violon. Par exemple, si l'on joue un ''do''<sup>1</sup><ref>Pour la notation des octaves, voir ''[[../Représentation_musicale#Désignation_des_octaves|Représentation musicale > Désignation des octaves]]''.</ref> (fondamentale), on entend le ''do''<sup>2</sup> (une octave plus aigu), puis un ''sol''<sup>2</sup>, puis encore un ''do''<sup>3</sup> plus aigu, puis un ''mi''<sup>3</sup>, puis encore un ''sol''<sup>3</sup>, puis un ''si''♭<sup>3</sup>…
Ainsi, puisque lorsque l'on joue un ''do'' on entend aussi un ''sol'' très léger, alors jouer un ''do'' et un ''sol'' simultanément n'est pas choquant. De même pour ''do'' et ''mi''. De là vient la notion de consonance.
Le statut du ''si''♭ est plus ambigu. Il fait partie des harmoniques qui sonnent naturellement, mais il forme une seconde descendante avec le ''do'', intervalle dissonant. Par ailleurs, on remarque que le ''si''♭ ne fait pas partie de la gamme de ''do'' majeur, contrairement au ''sol'' et au ''mi''.
Pour le jeu sur les dissonances, on peut écouter par exemple la ''Toccata'' en ''ré'' mineur, op. 11 de Sergueï Prokofiev (1912).
: {{lien web |url=https://www.youtube.com/watch?v=AVpnr8dI_50 |titre=Yuja Wang Prokofiev Toccata |site=YouTube |date=2019-02-26 |consulté le=2021-12-19}}
== Contrepoint ==
Dans le chant grégorien, la notion d'accord n'existe pas. L'harmonie provient de la superposition de plusieurs mélodies, notamment dans ce que l'on appelle le « contrepoint ».
Le terme provient du latin ''« punctum contra punctum »'', littéralement « point par point », et désigne le fait que les notes de chaque voix se correspondent.
L'exemple le plus connu de contrepoint est le canon, comme par exemple ''Frère Jacques'' : chaque note d'un couplet correspond à une note du couplet précédent.
Certains morceaux sont bâtis sur une écriture « en miroir » : l'ordre des notes est inversé entre les deux voix, ou bien les intervalles sont inversés (« mouvement contraire » : une tierce montante sur une voix correspond à une tierce descendante sur l'autre).
On peut également citer le « mouvement oblique » (une des voix, le bourdon, chante toujours la même note) et le mouvement parallèle (les deux voix chantent le même air mais transposé, l'une est plus aiguë que l'autre).
Nous reproduisons ci-dessous le début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.
[[Fichier:Haendel Sonate en trio re mineur debut canon.svg | vignette | center | upright=2 | Début du second ''Allergo'' de la sonate en trio en ''ré'' mineur de Haendel.]]
[[Fichier:Haendel Sonate en trio re mineur debut.midi | vignette | Début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.]]
Nous avons mis en évidence la construction en canon avec des encadrés de couleur : sur les quatre premières mesures, nous voyons trois thèmes repris alternativement par une voix et par l'autre. Ce type de procédé est très courant dans la musique baroque.
Les procédés du contrepoint s'appliquent également à la danse :
* unisson : les danseurs et danseuses font les mêmes gestes en même temps ;
* répétition : le fait de répéter une série de gestes, une « phrase dansante » ;
* canon : les gestes sont faits avec un décalage régulier d'un danseur ou d'une danseuse à l'autre ;
* cascade : forme de canon dans laquelle le décalage est très petit ;
* contraste : deux danseur·euses, ou deux groupes, ont des gestuelles très différentes ;
* accumulation : la gestuelle se complexifie par l'ajout d'éléments au fur et à mesure ; ou bien le nombre de danseur·euses augmente ;
* dialogue : les gestes de danseur·euses ou de groupes se répondent ;
* contre-point : la gestuelle d'un ou une danseuse se superpose à la gestuelle d'un groupe ;
* lâcher-rattraper : les danseurs et danseuses alternent danse à l'unisson et gestuelles indépendantes.
: {{lien web
| url=https://www.youtube.com/watch?v=wgblAOzedFc
| titre=Les procédés de composition en danse
| auteur= Doisneau Sport TV
| site=YouTube
| date=2020-03-16 | consulté le=2021-01-21
}}
{{...}}
== Les accords en général ==
Initialement, on a des chants polyphoniques, des voix qui chantent chacune une mélodie, les mélodies se mêlant. On remarque que certaines superpositions de notes sonnent de manière plus ou moins agréables, consonantes ou dissonantes. On en vient alors à associer ces notes, c'est-à-dire à considérer dès le départ la superposition de ces notes et non pas la rencontre de ces notes au gré des mélodies. Ces groupes de notes superposées forment les accords. En Europe, cette notion apparaît vers le {{pc|xiv}}<sup>e</sup> siècle avec notamment la ''[[wikipedia:fr:Messe de Notre Dame|Messe de Notre Dame]]'' de Guillaume de Machaut (vers 1360-1365). La notion « d'accord parfait » est consacrée par [[wikipedia:fr:Jean-Philippe Rameau|Jean-Philippe Rameau]] dans son ''Traité de l'harmonie réduite à ses principes naturels'', publié en 1722.
=== Qu'est-ce qu'un accord ? ===
Un accord est un ensemble d'au minimum trois notes jouées en même temps. « Jouées » signifie qu'il faut qu'à un moment donné, elles sonnent en même temps, mais le début ou la fin des notes peut être à des instants différents.
Considérons que l'on joue les notes ''do'', ''mi'' et ''sol'' en même temps. Cet accord s'appelle « accord de ''do'' majeur ». En musique classique, on lui adjoint l'adjectif « parfait » : « accord parfait de ''do'' majeur ».
Nous représentons ci-dessous trois manière de faire l'accord : avec trois instruments jouant chacun une note :
[[Fichier:Do majeur trois portees.svg|class=transparent|center|Accord de ''do'' majeur avec trois instruments différents.]]
Avec un seul instrument jouant simultanément les trois notes :
[[Fichier:Chord C.svg|class=transparent|center|Accord de ''do'' majeur joué par un seul instrument.]]
L'accord tel qu'il est joué habituellement par une guitare d'accompagnement :
[[Fichier:Do majeur guitare.svg|class=transparent|center|Accord de ''do'' majeur à la guitare.]]
Pour ce dernier, nous représentons le diagramme indiquant la position des doigts sur le manche au dessus de la portée et la tablature en dessous. Ici, c'est au total six notes qui sont jouées : ''mi'' grave, ''do'' médium, ''mi'' médium, ''sol'' médium, ''do'' aigu, ''mi'' aigu. Mais il s'agit bien des trois notes ''do'', ''mi'' et ''sol'' jouées à des octaves différentes. Nous remarquons également que la note de basse (la note la plus grave), ''mi'', est différente de la note fondamentale (celle qui donne le nom à l'accord), ''do'' ; l'accord est dit « renversé » (voir plus loin).
=== Comment joue-t-on un accord ? ===
Les notes ne sont pas forcément jouées en même temps ; elles peuvent être « égrainées », jouée successivement, ce que l'on appelle un arpège. La partition ci-dessous montre six manières différentes de jouer un accord de ''la'' mineur à la guitare, plaqué puis arpégé.
[[Fichier:La mineur differentes executions.svg|class=transparent|center|Différentes exécution de l'accord de do majeur à la guitare.]]
[[Fichier:La mineur differentes executions midi.midi|vignette|Différentes exécution de l'accord de la mineur à la guitare.]]
Vous pouvez écouter l'exécution de cette partition avec le lecteur ci-contre.
Seuls les instruments polyphoniques peuvent jouer les accords plaqués : instruments à clavier (clavecin, orgue, piano, accordéon), les instruments à plusieurs cordes pincées (harpe, guitare ; violon, alto, violoncelle et contrebasse joués en pizzicati). Les instruments à corde frottés de la famille du violon peuvent jouer des notes par deux à l'archet mais pas plus du fait de la forme bombée du chevalet ; cependant, un mouvement rapide permet de jouer les quatre cordes de manière très rapprochée. Les instruments à percussion de type xylophone ou le tympanon permettent de jouer jusqu'à quatre notes simultanément en tenant deux baguettes (mailloches, maillets) par main.
Tous les instruments peuvent jouer des arpèges même si, dans le cas des instruments monodiques, les notes ne continuent pas à sonner lorsque l'on passe à la note suivante.
L'arpège peut être joué par l'instrument de basse (basson, violoncelle, contrebasse, guitare basse, pédalier de l'orgue…), notamment dans le cas d'une basse continue ou d'une ''{{lang|en|walking bass}}'' (« basse marchante » : la basse joue des noires, donnant ainsi l'impression qu'elle marche).
En jazz, et spécifiquement au piano, on a recours au ''{{lang|en|voicing}}'' : on choisit la manière dont on organise les notes pour donner une couleur spécifique, ou bien pour créer une mélodie en enchaînant les accords. Il est fréquent de ne pas jouer toutes les notes : si on n'en garde que deux, ce sont la tierce et la septième, car ce sont celles qui caractérisent l'accord (selon que la tierce est mineure ou majeure, que la septième est majeure ou mineure), et la fondamentale est en général jouée par la contrebasse ou guitare basse.
{{clear}}
=== Classes d'accord ===
[[Fichier:Intervalles harmoniques accords classes.svg|vignette|upright=1.5|Intervalles harmoniques dans les accords classés de trois, quatre et cinq notes.]]
Un accord composé d'empilement de tierces est appelé « accord classé ». En musique tonale, c'est-à-dire la musique fondée sur les gammes majeures ou mineures (cas majoritaire en musique classique), on distingue trois classes d'accords :
* les accords de trois notes, ou triades, ou accords de quinte ;
* les accords de quatre notes, ou accords de septième ;
* les accords de cinq notes, ou accords de neuvième.
En empilant des tierces, si l'on part de la note fondamentale, on a donc de intervalles de tierce, quinte, septième et neuvième.
En musique tonale, les accords avec d'autres intervalles (hors renversement, voir ci-après), typiquement seconde, quarte ou sixte, sont considérés comme des transitions entre deux accords classés. Ils sont appelés, selon leur utilisation, « accords à retard » (en anglais : ''{{lang|en|suspended chord}}'', accord suspendu) ou « appoggiature » (note « appuyée », étrangère à l'harmonie). Voir aussi plus loin la notion de note étrangère.
=== Renversements d'accords ===
[[File:Accord do majeur renversements.svg|thumb|Accord parfait de do majeur et ses renversements.]]
[[Fichier:Progression dominante renverse parfait do majeur.svg|vignette|upright=0.6|Progression accord de dominante renversé → accord parfait en ''do'' majeur.]]
Un accord classé est donc un empilement de tierces. Si l'on change l'ordre des notes, on a toujours le même accord mais il est fait avec d'autres intervalles harmoniques. Par exemple, l'accord parfait de ''do'' majeur dans son état fondamental, c'est-à-dire non renversé, s'écrit ''do'' - ''mi'' - ''sol''. Sa note fondamentale, ''do'', est aussi se note de basse.
Si maintenant on prend le ''do'' de l'octave supérieure, l'accord devient ''mi - sol - do'' ; c'est l'empilement d'une tierce ''(mi - sol)'' et d'une quarte ''(sol - do)'', soit la superposition d'une tierce ''(mi - sol)'' et d'une sixième ''(mi - do)''. C'est le premier renversement de l'accord parfait de ''do'' majeur ; la fondamentale est toujours ''do'' mais la basse est ''mi''. Le second renversement est ''sol - do - mi''.
L'utilisation de renversement peut faciliter l'exécution de la progression d'accord. Par exemple, en tonalité ''do'' majeur, si l'on veut passer de l'accord de dominante ''sol - si - ré'' à l'accord parfait ''do - mi - sol'', alors on peut utiliser le second renversement de l'accord de dominante : ''ré - sol - si'' → ''do - mi - sol''. Ainsi, la basse descend juste d'un ton ''(ré → do)'' et sur un piano, la main reste globalement dans la même position.
Le renversement d'un accord permet également de respecter certaines règles de l'harmonie classique, notamment éviter que des voix se suivent strictement (« mouvement parallèle »), ce qui aurait un effet de platitude.
De manière générale, la notion de renversement permet deux choses :
* d'enrichir l'œuvre : pour créer une harmonie donnée (c'est-à-dire des sons sonnant bien ensemble), nous avons plus de souplesse, nous pouvons organiser ces notes comme nous le voulons selon les voix ;
* de simplifier l'analyse : quelle que soit la manière dont sont organisées les notes, cela nous ramène à un même accord.
{{citation bloc|Or il, y a plusieurs manières de jouer un accord, selon que l'on aborde par la première note qui le constitue, ''do mi sol'', la deuxième, ''mi sol do'', ou la troisième note, ''sol do mi''. Ce sont les renversements, [que Rameau] va classer en différentes combinaisons d'une seule matrice. Faisant cela, Rameau divise le nombre d'accords [de septième] par quatre. Il simplifie, il structure […].|{{ouvrage|prénom1=André |nom1=Manoukian |titre=Sur les routes de la musique |éditeur=Harper Collins |année=2021 |passage=54 |isbn=979-1-03391201-9}} }}
{{clear}}
[[File:Plusieurs realisation 1er renversement doM.svg|thumb|Plusieurs réalisation du premier renversement de l'accord de ''do'' majeur.]]
Notez que dans la notion de renversement, seule importe en fait la note de basse. Ainsi, les accords ''mi-sol-do'', ''mi-do-sol'', ''mi-do-mi-sol'', ''mi-sol-mi-do''… sont tous une déclinaison du premier renversement de ''do-mi-sol'' et ils seront abrégés de la même manière (''mi''<sup>6</sup> en musique classique ou C/E en musique populaire et jazz, voir plus bas).
{{clear}}
== Notation des accords de trois notes ==
Les accords de trois notes sont appelés « accords de quinte » en classique, et « triades » en jazz.
[[Fichier:Progression dominante renverse parfait do majeur chiffrage.svg|vignette|upright=0.7|Chiffrage du second renversement d'un accord de ''sol'' majeur et d'un accord de ''do'' majeur : notation en musique populaire et jazz (haut) et notation de basse chiffrée (bas).]]
Les accords sont construits de manière systématique. Nous pouvons donc les représenter de manière simplifiée. Cette notation simplifiée des accords est appelée « chiffrage ».
Reprenons la progression d'accords ci-dessus : « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » dans la tonalité de ''do'' majeur. On utilise en général trois notations différentes :
* en musique populaire, jazz, rock… un accord est désigné par sa note fondamentale ; ici donc, les accords sont notés « ''sol'' - ''do'' » ou, en notation anglo-saxonne, « G - C » ;<br /> comme le premier accord est renversé, on indique la note de basse après une barre, la progression d'accords est donc chiffrée '''« ''sol''/''ré'' - ''do'' »''' ou '''« G/D - C »''' ;<br /> il s'agit ici d'accords composés d'une tierce majeure et d'une quinte juste ; si les accords sont constitués d'intervalles différents, nous ajoutons un symbole après la note : « m » ou « – » si la tierce est mineure, « dim » ou « ° » si la quinte est diminuée ;
* en musique classique, on utilise la notation de « basse chiffrée » (utilisée notamment pour noter la basse continue en musique baroque) : on indique la note de basse sur la portée et on lui adjoint l'intervalle de la fondamentale à la note la plus haute (donc ici respectivement 6 et 5, puisque ''sol''-''si'' est une sixte et ''do''-''sol'' est une quinte), étant sous-entendu que l'on a des empilements de tierce en dessous ; mais dans le cas du premier accord, le premier intervalle n'est pas une tierce, mais une quarte ''(ré''-''sol)'', on note donc '''« ''ré'' <sup>6</sup><sub>4</sub> - ''do'' <sup>5</sup> »'''<ref>quand on ne dispose pas de la notation en supérieur (exposant) et inférieur (indice), on utilise parfois une notation sous forme de fraction : ''sol'' 6/4 et ''do'' 5/.</ref> ;
* lorsque l'on fait l'analyse d'un morceau, on s'attache à identifier la note fondamentale de l'accord (qui est différente de la basse dans le cas d'un renversement) ; on indique alors le degré de la fondamentale : '''« {{Times New Roman|V<sup>6</sup><sub>4</sub> - I<sup>5</sup>}} »'''.
La notation de basse chiffrée permet de construire l'accord à la volée :
* on joue la note indiquée (basse) ;
* s'il n'y a pas de 2 ni de 4, on lui ajoute la tierce ;
* on ajoute les intervalles indiqués par le chiffrage.
La notation de musique jazz oblige à connaître la composition des différents accords, mais une fois que ceux-ci sont acquis, il n'y a pas besoin de reconstruire l'accord.
La notation de basse chiffrée avec les chiffres romains n'est pas utilisée pour jouer, mais uniquement pour analyser ; Sur les partitions avec basse chiffrée, il y a simplement les chiffrages indiqués au-dessus de la partie de basse. Le chiffrage avec le degré en chiffres romains présente l'avantage d'être indépendant de la tonalité et donc de se concentrer sur la fonction de l'accord au sein de la tonalité. Par exemple, ci-dessous, nous pouvons parler de la progression d'accords « {{Times New Roman|V - I}} » de manière générale, cette notation étant valable quelle que soit la tonalité.
[[File:Progression dominante renverse parfait do majeur chiffrage basse continue.svg|thumb|Chiffrage en notation basse chiffrée de la progression d'accords « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » en do majeur.]]
{{note|En notation de base continue avec fondamentale en chiffres romains, la fondamentale est toujours indiquée ''sous'' la portée de la partie de basse. Les intervalles sont indiqués au-dessus de la portée de la partie de basse ; lorsque l'on fait une analyse, on peut ayssi les indiquer à côté du degré en chiffres romains, donc sous la portée de la basse.}}
{{note|En notation rock, le 5 en exposant indique un accord incomplet avec uniquement la fondamentale et la quinte, un accord sans tierce appelé « accord de puissance » ou ''{{lang|en|power chord}}''. Par exemple, C<sup>5</sup> est l'accord ''do-sol''.}}
{{clear}}
[[Fichier:Accords parfait do majeur basse chiffree fondamental et renverse.svg|vignette|upright=2.5|Chiffrage de l'accord parfait de ''do'' majeur en basse chiffrée, à l'état fondamental et ses renversements.]]
Concernant les accords parfaits en notation de basse chiffrée :
* un accord parfait à l'état fondamental est chiffré « <sup>5</sup> » ; on l'appelle « accord de quinte » ;
* le premier renversement est chiffré « <sup>6</sup> » (la tierce est implicite) ; on l'appelle « accord de sixte » ;
* le second renversement est noté « <sup>6</sup><sub>4</sub> » ; on l'appelle « accord de sixte et de quarte » (ou bien « de quarte et de sixte »).
Par exemple, pour l'accord parfait de ''do'' majeur :
* l'état fondamental ''do''-''mi''-''sol'' est noté ''do''<sup>5</sup> ;
* le premier renversement ''mi''-''sol''-''do'' est noté ''mi''<sup>6</sup> ;
* le second renversement ''sol''-''do''-''mi'' est noté ''sol''<sup>6</sup><sub>4</sub>.
Il y a une exception : l'accord construit sur la sensible (7{{e}} degré) contient une quinte diminuée et non une quinte juste. Le chiffrage est donc différent :
* l'état fondamental ''si''-''ré''-''fa'' est noté ''si''<sup><s>5</s></sup> (cinq barré), « accord de quinte diminuée » ;
* le premier renversement ''ré''-''fa''-''si'' est noté ''ré''<sup>+6</sup><sub>3</sub>, « accord de sixte sensible et tierce » ;
* le second renversement ''fa''-''si''-''ré'' est noté ''fa''<sup>6</sup><sub>+4</sub>, « accord de sixte et quarte sensible ».
Par ailleurs, on ne considère pas qu'il est fondé sur la sensible, mais sur la dominante ; on met donc des guillemets autour du degré, « “V” ». Donc selon l'état, le chiffrage est “V”<sup><s>5</s></sup>, “V”<sup>+6</sup><sub>3</sub> ou “V”<sup>6</sup><sub>+4</sub>.
En notation jazz, on ajoute « dim », « <sup>o</sup> » ou bien « <sup>♭5</sup> » au chiffrage, ici : B dim, B<sup>o</sup> ou B<sup>♭5</sup> pour l'état fondamental. Pour les renversements : B dim/D et B dim/F ; ou bien B<sup>o</sup>/D et B<sup>o</sup>/F ; ou bien B<sup>♭5</sup>/D et B<sup>♭5</sup>/F.
{{clear}}
[[Fichier:Accords basse chiffree basse do fondamental et renverses.svg|vignette|upright=2|Basse chiffrée : accords de quinte, de sixte et de sixte et de quarte ayant pour basse ''do''.]]
Et concernant les accords ayant pour basse ''do'' en tonalité de ''do'' majeur :
* l'accord ''do''<sup>5</sup> est un accord à l'état fondamental, c'est donc l'accord ''do''-''mi''-''sol'' (sa fondamentale est ''do'') ;
* l'accord ''do''<sup>6</sup> est le premier renversement d'un accord, c'est donc l'accord ''do''-''mi''-''la'' (sa fondamentale est ''la'') ;
* l'accord ''do''<sup>6</sup><sub>4</sub> est le second renversement d'un accord, c'est donc l'accord ''do''-''fa''-''la'' (sa fondamentale est ''fa'').
{{clear}}
== Notes étrangères ==
La musique européenne s'appuie essentiellement sur des accords parfaits, c'est-à-dire fondés sur une tierce majeure ou mineure, et une quinte juste. Il arrive fréquemment qu'un accord ne soit pas un accord parfait. Les notes qui font partie de l'accord parfait sont appelées « notes naturelles » et la note qui n'en fait pas partie est appelée « note étrangère ».
Il existe plusieurs types de notes étrangères :
* anticipation : la note étrangère est une note naturelle de l'accord suivant ;
* appogiature : note d'ornementation qui se résout par mouvement conjoint, c'est-à-dire qu'elle est suivie par une note située juste au-dessus ou en dessous (seconde ascendante ou descendante) qui est, elle, une note naturelle ;
* broderie : on part d'une note naturelle, on monte ou on descend d'une seconde, puis on revient sur la note naturelle ;
* double broderie : on part d'une note naturelle, on joue la note du dessus puis la note du dessous avant de revenir à la note naturelle ; ou bien on joue la note du dessous puis la note du dessus ;
* échappée : note étrangère n'appartenant à aucune des autres catégories ;
* note de passage : mouvement conjoint allant d'une note naturelle d'un accord à une note naturelle de l'accord suivant ;
* pédale : la note de basse reste la même pendant plusieurs accords successifs ;
* retard : la note étrangère est une note naturelle de l'accord précédent.
Les notes étrangères ne sont pas chiffrées.
[[File:Notes etrangeres accords.svg|center|Différents types de notes étrangères.]]
{{note|Les anglophones distinguent deux types de retard : la ''{{lang|en|suspension}}'' est résolue vers le haut (le mouvement est ascendant), le ''{{lang|en|retardation}}'' est résolu vers le bas (le mouvement est descendant).}}
== Principaux accords ==
Les trois principaux accords sont :
* l'accord parfait majeur : il est construit sur les degrés {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (médiante) et {{Times New Roman|V}} (dominante) d'une gamme majeure ; il est noté {{Times New Roman|I}}<sup>5</sup>, {{Times New Roman|IV}}<sup>5</sup>, {{Times New Roman|V}}<sup>5</sup> ;
* l'accord parfait mineur : il est construit sur les degrés {{Times New Roman|I}} (tonique) et {{Times New Roman|IV}} (sous-tonique) d'une gamme mineure harmonique ; il est également noté {{Times New Roman|I}}<sup>5</sup> et {{Times New Roman|IV}}<sup>5</sup>, les anglo-saxons le notent {{Times New Roman|i}}<sup>5</sup> et {{Times New Roman|iv}}<sup>5</sup> (la minuscule indiquant le caractère mineur) ;
* l'accord de septième de dominante : il est construit sur le degré {{Times New Roman|V}} (dominante) d'une gamme majeure ou mineure harmonique ; il est noté {{Times New Roman|V}}<sup>7</sup><sub>+</sub>.
On peut trouver ces trois accords sur d'autres degrés, et il existe d'autre types d'accords. Nous verrons cela plus loin.
{| class="wikitable"
|+ Constitution des principaux accords — Dénomination classique
|-
! scope="col" | Accord
! scope="col" | 1<sup>er</sup> intervalle
! scope="col" | 2<sup>e</sup> intervalle
! scope="col" | 3<sup>e</sup> intervalle
|-
! scope="row" | Accord parfait majeur
| tierce majeure (3M) || quinte juste (5J) || —
|-
! scope="row" | Accord parfait mineur
| tierce mineure (3m) || quinte juste (5J) || —
|-
! scope="row" | Accord de septième de dominante
| tierce majeure (3M) || quinte juste (5J) || septième mineure (7m)
|}
{| class="wikitable"
|+ Constitution des principaux accords — Dénomination jazz
|-
! scope="col" | Accord
! scope="col" | 1<sup>er</sup> intervalle
! scope="col" | 2<sup>e</sup> intervalle
! scope="col" | 3<sup>e</sup> intervalle
|-
! scope="row" | Triade majeure
| tierce majeure (3M) || quinte juste (5J) || —
|-
! scope="row" | Triade mineure
| tierce mineure (3m) || quinte juste (5J) || —
|-
! scope="row" | Accord de septième
| tierce majeure (3M) || quinte juste (5J) || septième mineure (7m)
|}
{| border="0"
|-
| [[Fichier:Accord do majeur arpege puis plaque.midi | Accord parfait de ''do'' majeur (C).]] || [[Fichier:Accord do mineur arpege puis plaque.midi | Accord parfait de ''do'' mineur (Cm).]] || [[Fichier:Accord do septieme arpege puis plaque.midi | Accord de septième de dominante de ''fa'' majeur (C<sup>7</sup>).]]
|-
| Accord parfait<br /> de ''do'' majeur (C). || Accord parfait<br /> de ''do'' mineur (Cm). || Accord de septième de dominante<br /> de ''fa'' majeur (C<sup>7</sup>).
|}
'''Rappel :'''
* la tierce mineure est composée d'un ton et demi (1 t ½) ;
* la tierce majeur est composée de deux tons (2 t) ;
* la quinte juste a la même altération que la fondamentale, sauf lorsque la fondamentale est ''si'' (la quinte juste est alors ''fa''♯) ;
* la septième mineure est le renversement de la seconde majeure (1 t).
[[File:Renversements accords pft fa maj basse chiffree.svg|thumb|Renversements de l'accord parfait de ''fa'' majeur, et la notation de basse chiffrée.]]
[[File:Renversements accord sept de dom fa maj basse chiffree.svg|thumb|Renversements de l'accord de septième de dominante de ''fa'' majeur, et la notation de basse chiffrée.]]
{| class="wikitable"
|+ Notation des principaux accords en musique classique
|-
! scope="col" | Accord
! scope="col" | État<br /> fondamental
! scope="col" | Premier<br /> renversement
! scope="col" | Deuxième<br /> renversement
! scope="col" | Troisième<br /> renversement
|-
! scope="row" | Accord parfait
| {{Times New Roman|I<sup>5</sup>}}<br/> acc. de quinte || {{Times New Roman|I<sup>6</sup>}}<br :> acc. de sixte || {{Times New Roman|I<sup>6</sup><sub>4</sub>}}<br /> acc. de quarte et de sixte || —
|-
! scope="row" | Accord de septième<br /> de dominante
| {{Times New Roman|V<sup>7</sup><sub>+</sub>}}<br /> acc.de septième de dominante || {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}<br />acc. de sixte et quinte diminuée || {{Times New Roman|V<sup>+6</sup>}}<br />acc. de sixte sensible || {{Times New Roman|V<sup>+4</sup>}}<br />acc. de quarte sensible<br />acc. de triton
|}
{| class="wikitable"
|+ Notation des principaux accords en jazz
|-
! scope="col" | Accord
! scope="col" | Tierce
! scope="col" | Quinte
! scope="col" | Septième
! scope="col" | Chiffrage
|-
! scope="row" | Triade majeure
| 3M || 5J || || X
|-
! scope="row" | Triade mineure
| 3m || 5J || || Xm, X–
|-
! scope="row" | Septième
| 3M || 5J || 7m || X<sup>7</sup>
|}
En jazz, les renversements se notent en mettant la basse après une barre de fraction, par exemple pour la triade de ''do'' majeur :
* état fondamental : C ;
* premier renversement : C/E ;
* second renversement : C/G.
{{clear}}
Dans le cas d'un accord de septième de dominante, le nom de l'accord change selon que l'on est en musique classique ou en jazz : en musique classique, on donne le nom de la tonalité alors qu'en jazz, on donne le nom de la fondamentale. Ainsi, l'accord appelé « septième de dominante de ''do'' majeur » en musique classique, est appelé « ''sol'' sept » (G<sup>7</sup>) en jazz : la dominante (degré {{Times New Roman|V}}, dominante) de la tonalité de ''do'' majeur est la note ''sol''.
Comment appelle-t-on en musique classique l'accord appelé « ''do'' sept » (C<sup>7</sup>) en jazz ? Les tonalités dont le ''do'' est la dominante sont les tonalités de ''fa'' majeur (''si''♭ à la clef) et de ''fa'' mineur harmonique (''si''♭, ''mi''♭, ''la''♭ et ''ré''♭ à la clef et ''mi''♮ accidentel). Il s'agit donc de l'accord de septième de dominante des tonalités de ''fa'' majeur et ''fa'' mineur harmonique.
{| class="wikitable"
|+ Accords fréquents pour quelques la tonalités majeures
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Accord parfait<br />{{Times New Roman|I<sup>5</sup>}}
! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}}
|-
|''Do'' majeur || || C<br />''do-mi-sol'' || G7<br />''sol-si-ré-fa''
|-
|''Sol'' majeur || ''fa''♯ || G<br />''sol-si-ré'' || D7<br />''ré-fa''♯''-la-do''
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || D<br />''ré-fa''♯''-la'' || A7<br />''la-do''♯''-mi-sol''
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A<br />''la-do''♯''-mi'' || E7<br />''mi-sol''♯''-si-ré''
|-
| ''Fa'' majeur || ''si''♭ || F<br />''fa-la-do'' || C7<br />''do-mi-sol-si''♭
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭<br />''si''♭''-ré-fa'' || F7<br />''fa-la-do-mi''♭
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭<br />''mi''♭''-sol-si''♭ || B♭7<br />''si''♭''-ré-fa-la''♭
|}
{| class="wikitable"
|+ Accords fréquents pour quelques la tonalités mineures harmoniques
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Accord parfait<br />{{Times New Roman|i<sup>5</sup>}}
! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}}
|-
|''La'' mineur<br />harmonique || || Am, A–<br />''la-do-mi'' || E7<br />''mi-sol''♯''-si-ré''
|-
|''Mi'' mineur<br />harmonique || ''fa''♯ || Em, E–<br />''mi-sol-si'' || B7<br />''si-ré''♯''-fa''♯''-la''
|-
|''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || Bm, B–<br />''si-ré-fa''♯ || F♯7<br />''fa''♯''la''♯''-do''♯''-mi''
|-
|''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || F♯m, F♯–<br />''fa''♯''-la-do''♯ || C♯7<br />''do''♯''-mi''♯''-sol''♯''-si''
|-
| ''Ré'' mineur<br />harmonique || ''si''♭ || Dm, D–<br />''ré-fa-la'' || A7<br />''la-do''♯''-mi-sol''
|-
| ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || Gm, G–<br />''sol-si''♭''-ré'' || D7<br />''ré-fa''♯''-la-do''
|-
| ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || Cm, C–<br />''do-mi''♭''-sol'' || G7<br />''sol-si''♮''-ré-fa''
|}
{{clear}}
== Accords sur les degrés d'une gamme ==
=== Harmonisation d'une gamme ===
[[Fichier:Accord trois notes gamme do majeur chiffre.svg|vignette|upright=1.2|Accords de trois note sur la gamme de ''do'' majeur, chiffrés.]]
On peut ainsi construire une triade par degré d'une gamme.
Pour une gamme majeure, les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|V<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|II<sup>5</sup>}}, {{Times New Roman|III<sup>5</sup>}}, {{Times New Roman|VI<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|ii<sup>5</sup>}}, {{Times New Roman|iii<sup>5</sup>}}, {{Times New Roman|vi<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}.
Les accords ont tous une quinte juste à l'exception de l'accord {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} qui a une quinte diminuée, raison pour laquelle le « 5 » est barré. C'est un accord dit « de quinte diminuée ». En jazz, l'accord diminué est noté « dim », « ° », « m<sup>♭5</sup> » ou « <sup>–♭5</sup> ».
Nous avons donc trois types d'accords (dans la notation jazz) : X (triade majeure), Xm (triade mineure) et X° (triade diminuée), la lettre X remplaçant le nom de la note fondamentale.
{{clear}}
[[Fichier:Accord trois notes gamme la mineur chiffre.svg|vignette|upright=1.2|Accords de trois notes sur une gamme de ''la'' mineur harmonique, chiffrés.]]
Pour une gamme mineure harmonique, les accords {{Times New Roman|III<sup>+5</sup>}}, {{Times New Roman|V<sup>♯</sup>}} et {{Times New Roman|VI<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|II<sup><s>5</s></sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|i<sup>5</sup>}}, {{Times New Roman|ii<sup><s>5</s></sup>}}, {{Times New Roman|iv<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}.
Les accords {{Times New Roman|ii<sup><s>5</s></sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} ont une quinte diminuée ; ce sont des accords dits « de quinte diminuée ». L'accord {{Times New Roman|III<sup>+5</sup>}} a une quinte augmentée ; le signe « plus » indique que la note de cinquième, le ''sol'' dièse, est la sensible. En jazz, l'accord est noté « aug » ou « <sup>+</sup> ». Les autres accords ont une quinte juste.
Aux trois accords générés par une gamme majeure (X, Xm et X°), nous voyons ici apparaître un quatrième type d'accord : la triade augmentée X<sup>+</sup>.
Nous remarquons que des gammes ont des accords communs. Par exemple, l'accord {{Times New Roman|ii<sup>5</sup>}} de ''do'' majeur est identique à l'accord {{Times New Roman|iv<sup>5</sup>}} de ''la'' mineur (il s'agit de l'accord Dm).
Quel que soit le mode, les accords construits sur la sensible (accord de quinte diminuée) sont rarement utilisés. S'ils le sont, c'est en tant qu'accord de septième de dominante sans fondamentale (voir ci-après). C'est la raison pour laquelle le chiffrage indique le degré {{Times New Roman|V}} entre guillemets, et non pas le degré {{Times New Roman|VII}} (mais pour des raisons de clarté, nous l'indiquons entre parenthèses au début).
En mode mineur, l'accord de quinte augmentée {{Times New Roman|iii<sup>+5</sup>}} est très peu utilisé (voir plus loin ''[[#Progression_d'accords|Progression d'accords]]''). C'est un accord considéré comme dissonant.
On voit que :
* un accord parfait majeur peut appartenir à cinq gammes différentes ;<br /> par exemple l'accord parfait de ''do'' majeur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> degré de la gamme de ''do'' majeur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' mineur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''mi'' mineur ;
* un accord parfait mineur peut appartenir à cinq gammes différentes ;<br />par exemple l'accord parfait de ''la'' mineur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> de la gamme de ''la'' mineur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''mi'' mineur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|III}}<sup>e</sup> degré de ''fa'' majeur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''do'' majeur ;
* un accord de quinte diminuée peut appartenir à trois gammes différentes ;<br />par exemple, l'accord de quinte diminuée de ''si'' est l'accord construit sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' majeur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''la'' mineur et sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' mineur ;
* un accord de quinte augmentée (à l'état fondamental) ne peut appartenir qu'à une seule gamme ;<br /> par exemple, l'accord de quinte augmentée de ''do'' est l'accord construit sur le {{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur.
=== Harmonisation par des accords de septième ===
[[Fichier:Harmonisation gamme do majeur par septiemes chiffre.svg|vignette|upright=2|Harmonisation de la gamme de do majeur par des accords de septième.]]
Les accords de septième contiennent une dissonance et créent ainsi une tension. Ils sont très utilisés en jazz. Nous avons représenté ci-contre l'harmonisation de la gamme de ''do'' majeur.
La constitution des accords est la suivantes :
* tierce majeure (3M)
** quinte juste (5J)
*** septième mineure (7m) : sur le degré V, c'est l'accord de septième de dominante V<sup>7</sup><sub>+</sub>, noté X<sup>7</sup> (X pour G),
*** septième majeure (7M) : sur les degrés I et IV, appelés « accords de septième majeure » et notés aussi X<sup>maj7</sup> ou X<sup>Δ</sup> (X pour C ou F) ;
* tierce mineure (3m)
** quinte juste (5J)
*** septième mineure : sur les degrés ii, iii et vi, appelés « accords mineur septième » et notés Xm<sup>7</sup> ou X–<sup>7</sup> (X pour D, E ou A),
** quinte diminuée (5d)
*** septième mineure (7m) : sur le degré vii, appelé « accord demi-diminué » (puisque seule la quinte est diminuée) et noté X<sup>∅</sup> ou Xm<sup>7(♭5)</sup> ou X–<sup>7(♭5)</sup> (X pour B) ;<br /> en musique classique, on considère que c'est un accord de neuvième de dominante sans fondamentale.
Nous avons donc quatre types d'accords : X<sup>7</sup>, X<sup>maj7</sup>, Xm<sup>7</sup> et X<sup>∅</sup>
En jazz, on ajoute souvent la quarte à l'accord de sous-dominante IV (sur le ''fa'' dans une gamme de ''do'' majeur) ; il s'agit ici d'une quarte augmentée (''fa''-''si'') et l'accord est surnommé « accord lydien » mais cette dénomination est erronée (il s'agit d'une mauvaise interprétation de textes antiques). C'est un accord de onzième sans neuvième (la onzième étant l'octave de la quarte), il est noté X<sup>maj7(♯11)</sup> ou X<sup>Δ(♯11)</sup> (ici, F<sup>maj7(♯11)</sup>, ''fa''-''la''-''do''-''mi''-''si'' ou ''fa''-''la''-''si''-''do''-''mi'').
=== Modulation et emprunt ===
Un morceau peut comporter des changements de tonalité; appelés « modulation ». Il y a parfois un court passage dans une tonalité différente, typiquement sur une ou deux mesures, avant de retourner dans la tonalité d'origine : on parle d'emprunt. Lorsqu'il y a une modulation ou un emprunt, les degrés changent. Un même accord peut donc avoir une fonction dans une partie du morceau et une autre fonction ailleurs. L'utilisation d'accord différents, et en particulier d'accord utilisant des altérations accidentelles, indique clairement une modulation.
Nous avons vu précédemment que les modulations courantes sont :
* les modulations dans les tons voisins ;
* les modulations homonymes ;
* les marches harmoniques.
Une modulation entre une tonalité majeure et mineure change la couleur du passage,
* la modulation la plus « douce » est entre les tonalités relatives (par exemple''do'' majeur et ''la'' mineur) car ces tonalités utilisent quasiment les mêmes notes ;
* la modulation la plus « voyante » est la modulation homonyme (par exemple entre ''do'' majeur et ''do'' mineur).
Une modulation commence souvent sur l'accord de dominante de la nouvelle tonalité.
Pour analyser un œuvre, ou pour improviser sur une partie, il est important de reconnaître les modulations. La description de la successind es tonalités s'appelle le « parcours tonal ».
=== Exercices élémentaires ===
L'apprentissage des accords passe par quelques exercices élémentaires.
'''1. Lire un accord'''
Il s'agit de lecture de notes : des notes composant les accords sont écrites « empilées » sur une portée, il faut les lire en énonçant les notes de bas en haut.
'''2. Reconnaître la « couleur » d'un accord'''
On écoute une triade et il faut dire si c'est une triade majeure ou mineure. Puis, on complexifie l'exercice en ajoutant la septième.
'''3. Chiffrage un accord'''
Trouver le nom d'un accord à partir des notes qui le composent.
'''4. Réalisation d'un accord'''
Trouver les notes qui composent un accord à partir de son nom.
'''5. Dictée d'accords'''
On écoute une succession d'accords et il faut soit écrire les notes sur une portée, soit écrire les noms de accords.
[[File:Exercice constitution accord basse chiffree.svg|thumb|Exercice : constitution d'accord à partir de la basse chiffrée.]]
'''Exercices de basse chiffrée'''
''Réalisation d'un accord''
Sur la figure suivante, écrire les notes des accords correspondant à la basse chiffrée. Déterminer le degré de la fondamentale pour chaque accord en considérant que nous sommes dans la tonalité de ''sol'' majeur.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice constitution accord basse chiffree solution.svg|vignette|Solution.]]
# La note de basse est un ''do''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''mi'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''sol''.<br />Le chiffrage « <sup>5</sup> » indique que c'est un accord dans son état fondamental (l'écart entre deux notes consécutives ne dépasse pas la tierce), la fondamentale est donc la basse, ''do'', qui est le degré IV de la tonalité.
# La note de basse est un ''si''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''ré'', puis nous appliquons le chiffrage 6 et ajoutons la sixte, ''sol''.<br />Le chiffrage « <sup>6</sup> » indique que c'est un accord dans son premier renversement. En le remettant dans son état fondamental, nous obtenons ''sol-si-ré'', la fondamentale est donc la tonique, le degré I.
# La note de basse est un ''la''. Nous ajoutons la tierce (chiffre 3), ''do'', et la sixte (6), ''fa''♯. Nous vérifions que le ''fa''♯ est la sensible (signe +)<br />Nous voyons un « blanc » entre les notes ''do'' et ''fa''♯. En descendant le ''fa''♯ à l'octave inférieure, nous obtenons un empilement de tierces ''fa''♯-''la-do'', le fondamentale est donc ''fa''♯, le degré VII. Nous pouvons le voir comme le deuxième renversement de l'accord de septième de dominante, sans fondamentale.
# La note de basse est un ''fa''♯. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''la'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''do'' ; nous vérifions qu'il s'agit bien d'une quinte diminuée (le 5 est barré). Nous appliquons le chiffre 6 et ajoutons la sixte, ''ré''.<br />Nous voyons que les notes ''do'' et ''ré'' sont conjointes (intervalle de seconde). En descendant le ''ré'' à l'octave inférieure, nous obtenons un empilement de tierces ''ré-fa''♯-''la-do'', le fondamentale est donc ''ré'', le degré V. Nous constatons que l'accord chiffré est le premier renversement de l'accord de septième de dominante.
{{boîte déroulante/fin}}
{{clear}}
''Chiffrage d'accords''
[[Fichier:Exercice chiffrage accord basse chiffree.svg|vignette|Accords à chiffrer.]]
Chiffrer les accords ci-contre.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice chiffrage accord basse chiffree solution.svg|vignette|Solution.]]
# On relève les intervalles en partant de la basse : tierce majeure (3M) et quinte juste (5J). Le chiffrage complet est donc ''fa''<sup>5</sup><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''fa''<sup>5</sup>.<br /> On peut aussi reconnaître que c'est l'accord parfait sur la tonique de la tonalité de ''fa'' majeur dans son état fondamental, le chiffrage d'un accord parfait étant <sup>5</sup>.
# On relève les intervalles en partant de la basse : quarte juste (4J), sixte majeure (6M). Le chiffrage complet est donc ''fa''<sup>6</sup><sub>4</sub>.<br /> On peut aussi reconnaître que c'est le second renversement de l'accord ''mi-sol-si'', sur la tonique de la tonalité de ''mi'' mineur, le chiffrage du second renversement d'un accord parfait étant <sup>6</sup><sub>4</sub>.
# Les intervalles en partant de la basse sont : tierce mineure (3m), quinte diminuée (5d), sixte mineure (6m). Le chiffrage complet est donc ''mi''<sup>6</sup><small><s>5</s></small><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''mi''<sup>6</sup><sub><s>5</s></sub>.<br /> On reconnaît le premier renversement de l'accord ''do-mi-sol-si''♭, accord de septième de dominante de la tonalité de ''fa'' majeur.
# Les intervalles en partant de la basse sont : tierce mineure (3m), quinte juste (5J), septième mineure (7m). Le chiffrage complet est donc ''ré''<sup>7</sup><small>5</small><sub>3</sub> ; c'est typique d'un accord de septième de dominante, son chiffrage est donc ''ré''<sup>7</sup><sub>+</sub>.<br /> On reconnaît l'accord de septième de dominante de la tonalité de ''sol'' mineur dans son état fondamental.
{{boîte déroulante/fin}}
{{clear}}
[[File:Exercice constitution accord notation jazz.svg|thumb|Exercice : constitution d'un accord d'après son chiffrage en notation jazz.]]
'''Exercices de notation jazz'''
''Réalisation d'un accord''
Sur la figure suivante, écrire les notes des accords correspondant aux chiffrages.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice constitution accord notation jazz solution.svg|thumb|Solution.]]
# Il s'agit de la triade majeure de ''do'' dans son état fondamental. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''do-mi-sol''.
# Il s'agit de la triade majeure de ''sol''. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''sol-si-ré''. On renverse l'accord afin que la basse soit le ''si'', l'accord est donc ''si-ré-sol''.
# Il s'agit de l'accord demi-diminué de ''fa''♯. Les intervalles sont la tierce mineure (3m), la quinte diminuée (5d) et la septième mineure (7m). Les notes sont donc ''fa''♯-''la-do-mi''. Nous renversons l'accord afin que la basse soit le ''la'', l'accord est donc ''a-do-mi-fa''♯.
# Il s'agit de l'accord de septième de ''ré''. Les intervalles sont donc la tierce majeure (3M), la quinte juste (5J) et la septième mineure (7m). Les notes sont ''ré-fa''♯''-la-do''. Nous renversons l'accord afin que la basse soit le ''fa''♯, l'accord est donc ''fa''♯''-la-do-ré''.
{{boîte déroulante/fin}}
{{clear}}
''Chiffrage d'accords''
[[File:Exercice chiffrage accord notation jazz.svg|thumb|Accords à chiffrer.]]
Chiffrer les accords ci-contre.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice chiffrage accord notation jazz solution.svg|thumb|Solution.]]
# Les notes sont toutes sur des interlignes consécutifs, c'est donc un empilement de tierces ; l'accord est dans son état fondamental. Les intervalles sont une tierce majeure (''fa-la'' : 3M) et une quinte juste (''fa-do'' : 5J), c'est donc la triade majeure de ''fa''. Le chiffrage est F.
# Il y a un blanc dans l'empilement des notes, c'est donc un accord renversé. En permutant les notes pour n'avoir que des tierces, on trouve l'accord ''mi-sol-si''. Les intervalles sont une tierce mineure (''mi-sol'' : 3m) et une quinte juste (''mi-si'' : 5J), c'est donc la triade mineure de ''mi'' avec un ''si'' à la basse. Le chiffrage est Em/B ou E–/B.
# Il y deux notes conjointes, c'est donc un renversement. L'état fondamental de cet accord est ''do-mi-sol-si''♭. Les intervalles sont une tierce majeure (''do-mi'' : 3M), une quinte juste (''do-sol'' : 5J) et une septième mineure (''do-si''♭ : 7m). C'est donc l'accord de ''do'' septième avec un ''mi'' à la basse, chiffré C<sup>7</sup>/E.
# Les notes sont toutes sur des interlignes consécutifs, l'accord est dans son état fondamental. Les intervalles sont la tierce mineure (''ré-fa'' : 3m), une quinte juste (''ré-la'' : 5J) et une septième mineure (''ré-do'' : 7m). C'est donc l'accord de ''ré'' mineur septième, chiffré Dm<sup>7</sup> ou D–<sup>7</sup>.
{{boîte déroulante/fin}}
{{clear}}
== Harmonie fonctionnelle ==
Le choix des accords et de leur succession — la progression des accords — est un élément important d'un morceau, de sa composition. Le compositeur ou la compositrice a bien sûr une liberté totale, mais pour faire des choix, il faut comprendre les conséquences de ces choix, et donc ici, les effets produits par les accords et leur progression.
Une des manières d'aborder le sujet est l'harmonie fonctionnelle.
=== Les trois fonctions des accords ===
En harmonie tonale, on considère que les accords ont une fonction. Il existe trois fonctions :
* la fonction de tonique, {{Times New Roman|I}} ;
* la fonction de sous-dominante, {{Times New Roman|IV}} ;
* la fonction de dominante, {{Times New Roman|V}}.
L'accord de tonique, {{Times New Roman|I}}, est l'accord « stable » de la tonalité par excellence. Il conclut en général les morceaux, et ouvre souvent les morceaux ; il revient fréquemment au cours du morceau.
L'accord de dominante, {{Times New Roman|V}}, est un accord qui introduit une instabilité, une tension. En particulier, il contient la sensible (degré {{Times New Roman|VI}}), qui est une note « aspirée » vers la tonique. Cette tension, qui peut être renforcée par l'utilisation d'un accord de septième, est fréquemment résolue par un passage vers l'accord de tonique. Nous avons donc deux mouvements typiques : {{Times New Roman|I}} → {{Times New Roman|V}} (création d'une tension, d'une attente) et {{Times New Roman|V}} → {{Times New Roman|I}} (résolution d'une tension). Les accords de tonique et de dominante ont le cinquième degré en commun, cette note sert donc de pivot entre les deux accords.
L'accord de sous-dominante, {{Times New Roman|IV}}, est un accord qui introduit lui aussi une tension, mais moins grande : il ne contient pas la sensible. Notons que s'il est une quarte au-dessus de la tonique, il est aussi une quinte en dessous d'elle ; il est symétrique de l'accord de dominante. Il a donc un rôle similaire à l'accord de dominante, mais atténué. L'accord de sous-dominante aspire soit vers l'accord de dominante, très proche, et l'on a alors une augmentation de la tension ; soit vers l'accord de tonique, un retour vers la stabilité (il a alors un rôle semblable à la dominante). Du fait de ces deux bifurcations possibles — augmentation de la tension ({{Times New Roman|IV}} → {{Times New Roman|V}}) ou retour à la stabilité ({{Times New Roman|IV}} → {{Times New Roman|I}}) —, l'utilisation de l'accord de sous-dominante introduit un certain flottement : si l'on peut facilement prédire l'accord qui suit un accord de dominante, on ne peut pas prédire ce qui suit un accord de sous-dominante.
Notons que la composition ne consiste pas à suivre ces règles de manière stricte, ce qui conduirait à des morceaux stéréotypés et plats. Le plaisir d'écoute joue sur une alternance entre satisfaction d'une attente (respect des règles) et surprise (rompre les règles).
=== Accords remplissant ces fonctions ===
Les accords sur les autres degrés peuvent se ramener à une de ces trois fonctions :
* {{Times New Roman|II}} : fonction de sous-dominante {{Times New Roman|IV}} ;
* {{Times New Roman|III}} (très peu utilisé en mode mineur en raison de sa dissonance) et {{Times New Roman|VI}} : fonction de tonique {{Times New Roman|I}} ;
* {{Times New Roman|VII}} : fonction de dominante {{Times New Roman|V}}.
En effet, les accords étant des empilements de tierces, des accords situés à une tierce l'un de l'autre — {{Times New Roman|I}} ↔ {{Times New Roman|III}}, {{Times New Roman|II}} ↔ {{Times New Roman|IV}}, {{Times New Roman|V}} ↔ {{Times New Roman|VII}}, {{Times New Roman|VI}} ↔ {{Times New Roman|VIII}} ( = {{Times New Roman|I}}) — ont deux notes en commun. On retrouve le fait que l'accord sur le degré {{Times New Roman|VII}} est considéré comme un accord de dominante sans tonique. En mode mineur, l'accord sur le degré {{Times New Roman|III}} est évité, il n'a donc pas de fonction.
{|class="wikitable"
|+ Fonction des accords
|-
! scope="col" | Fondamentale
! scope="col" | Fonction
|-
| {{Times New Roman|I}} || tonique
|-
| {{Times New Roman|II}} || sous-dominante faible
|-
| {{Times New Roman|III}} || tonique faible
|-
| {{Times New Roman|IV}} || sous-dominante
|-
| {{Times New Roman|V}} || dominante
|-
| {{Times New Roman|VI}} || tonique faible
|-
| {{Times New Roman|VII}} || dominante faible
|}
Par exemple en ''do'' majeur :
* fonction de tonique : '''''do''<sup>5</sup> (C)''', ''mi''<sup>5</sup> (E–), ''la''<sup>5</sup> (A–) ;
* fonction de sous-dominante : '''''fa''<sup>5</sup> (F)''', ''ré''<sup>5</sup> (D–) ;
* fonction de dominante : '''''sol''<sup>5</sup> (G)''' ou ''sol''<sup>7</sup><sub>+</sub> (G<sup>7</sup>), ''si''<sup> <s>5</s></sup> (B<sup>o</sup>).
En ''la'' mineur harmonique :
* fonction de tonique : '''''la''<sup>5</sup> (A–)''', ''fa''<sup>5</sup> (F) [, rarement : ''do''<sup>+5</sup> (C<sup>+</sup>)] ;
* fonction de sous-dominante : '''''ré''<sup>5</sup> (D–)''', ''si''<sup> <s>5</s></sup> (B<sup>o</sup>) ;
* fonction de dominante : '''''mi''<sup>5</sup> (E)''' ou ''mi''<sup>7</sup><sub>+</sub> (E<sup>7</sup>), ''sol''♯<sup> <s>5</s></sup> (G♯<sup>o</sup>).
Le fait d'utiliser des accords différents pour remplir une fonction permet d'enrichir l'harmonie, et de jouer sur l'équilibre entre satisfaction d'une attente (on respecte les règles sur les fonctions) et surprise (mais on n'utilise pas l'accord attendu).
=== Les dominantes secondaires ===
On utilise aussi des accords de septième dominante se fondant sur un autre degré que la dominante de la gamme ; on parle de « dominante secondaire ». Typiquement, avant un accord de septième de dominante, on utilise parfois un accord de dominante de dominante, dont le degré est alors noté « {{Times New Roman|V}} de {{Times New Roman|V}} » ou « {{Times New Roman|V}}/{{Times New Roman|V}} » ; la fondamentale est de l'accord est alors situé cinq degrés au-dessus de la dominante ({{Times New Roman|V}}), c'est donc le degré {{Times New Roman|IX}}, c'est-à-dire le degré {{Times New Roman|II}} de la tonalité en cours). Ou encore, on utilise un accord de dominante du degré {{Times New Roman|IV}} (« {{Times New Roman|V}} de {{Times New Roman|IV}} », la fondamentale est alors le degré {{Times New Roman|I}}) avant un accord sur le degré {{Times New Roman|IV}} lui-même.
Par exemple, en tonalité de ''do'' majeur, on peut trouver un accord ''ré - fa''♯'' - la - do'' (chiffré {{Times New Roman|V}} de {{Times New Roman|V}}<sup>7</sup><sub>+</sub>), avant un accord ''sol - si - ré - fa'' ({{Times New Roman|V}}<sup>7</sup><sub>+</sub>). L'accord ''ré - fa''♯'' - la - do'' est l'accord de septième de dominante des tonalités de ''sol''. Dans la même tonalité, on pourra utiliser un accord ''do - mi - sol - si''♭ ({{Times New Roman|V}} de {{Times New Roman|IV}}<sup>7</sup><sub>+</sub>) avant un accord ''fa - la - do'' ({{Times New Roman|IV}}<sup>5</sup>). Le recours à une dominante secondaire peut atténuer une transition, par exemple avec un enchaînement ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> → ''fa''<sup>5</sup> (C → C<sup>7</sup> → F) qui correspond à un enchaînement {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}} : le passage ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> (C → C<sup>7</sup>) se fait en ajoutant une note (le ''si''♭) et rend naturel le passage ''do'' → ''fa''.
Sur les sept degré de la gamme, on ne considère en général que cinq dominantes secondaires : en effet, la dominante du degré {{Times New Roman|I}} est la dominante « naturelle, primaire » de la tonalité (et n'est donc pas secondaire) ; et utiliser la dominante de {{Times New Roman|VII}} consisterait à considérer l'accord de {{Times New Roman|VII}} comme un accord propre, on évite donc les « {{Times New Roman|V}} de “{{Times New Roman|V}}” » (mais les « “{{Times New Roman|V}}” de {{Times New Roman|V}} » sont tout à fait « acceptables »).
=== Enchaînements classiques ===
Nous avons donc vu que l'on trouve fréquemment les enchaînements suivants :
* pour créer une instabilité :
** {{Times New Roman|I}} → {{Times New Roman|V}},
** {{Times New Roman|I}} → {{Times New Roman|IV}} (instabilité moins forte mais incertitude sur le sens d'évolution) ;
* pour maintenir l'instabilité :
** {{Times New Roman|IV}} → {{Times New Roman|V}} ;
* pour résoudre l'instabilité :
** {{Times New Roman|IV}} → {{Times New Roman|I}},
** {{Times New Roman|V}} → {{Times New Roman|I}}, cas particuliers (voir plus bas) :
*** {{Times New Roman|V}}<sup>+4</sup> → {{Times New Roman|I}}<sup>6</sup>,
*** {{Times New Roman|I}}<sup>6</sup><sub>4</sub> → {{Times New Roman|V}}<sup>7</sup><sub>+</sub> → {{Times New Roman|I}}<sup>5</sup>.
Les degrés indiqués ci-dessus sont les fonctions ; on peut donc utiliser les substitutions suivantes :
* {{Times New Roman|I}} par {{Times New Roman|VI}} et, en tonalité majeure, {{Times New Roman|III}} ;
* {{Times New Roman|IV}} par {{Times New Roman|II}} ;
* {{Times New Roman|V}} par {{Times New Roman|VII}}.
Pour enrichir l'harmonie, on peut utiliser les dominantes secondaires, en particulier :
* {{Times New Roman|V}} de {{Times New Roman|V}} ({{Times New Roman|II}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|V}},
* {{Times New Roman|V}} de {{Times New Roman|IV}} ({{Times New Roman|I}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|IV}}.
On peut enchaîner les enchaînements, par exemple {{Times New Roman|I}} → {{Times New Roman|IV}} → {{Times New Roman|V}}, ou encore {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}}… En jazz, on utilise très fréquemment l'enchaînement {{Times New Roman|II}} → {{Times New Roman|V}} → {{Times New Roman|I}} (deux-cinq-un).
On peut bien sûr avoir d'autres enchaînements, mais ces règles permettent d'analyser un grand nombre de morceaux, et donnent des clefs utiles pour la composition. Nous voyons ci-après un certain nombre d'enchaînements courants dans différents styles
== Exercice ==
Un hautboïste travaille la sonate en ''do'' mineur S. 277 de Heinichen. Sur le deuxième mouvement ''Allegro'', il a du mal à travailler un passage en raison des altérations accidentelles. Sur la suggestion de sa professeure, il décide d'analyser la progression d'accords sous-jacente afin que les altérations deviennent logiques. Il s'agit d'un duo hautbois et basson pour lequel les accords ne sont pas chiffrés, le basson étant ici un instrument soliste et non pas un élément de la basse continue.
Sur l'extrait suivant, déterminez les basses et la qualité (chiffrage) des accords sous-jacents. Commentez.
[[Fichier:Sonate hautbois basson heinichen 2e mvt mes49.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen.]]
{{note|L'œuvre est en ''do'' mineur et devrait donc avoir trois bémols à la clef, or ici il n'y en a que deux. En effet, le ''la'' pouvant être bécarre en mode mineur mélodique ascendant, le compositeur a préféré le noter explicitement en altération accidentelle lorsque l'on est en mode mélodique naturel, harmonique ou mélodique descendant. C'est un procédé assez courant à l'époque baroque.}}
{{boîte déroulante/début|titre=Solution}}
Une des difficultés ici est que les arpèges joués par les instruments sont agrémentés de notes de passage.
Les notes de la basse (du basson) sont différentes entre le premier et le deuxième temps de chaque mesure et ne peuvent pas appartenir au même accord. On a donc un accord par temps.
Sur le premier temps de chaque mesure, le basson joue une octave. La note concernée est donc la basse de chaque accord. Pour savoir s'il s'agit d'un accord à l'état fondamental ou d'un renversement, on regarde ce que joue le hautbois : dans un mouvement conjoint (succession d'intervalles de secondes), il est difficile de distinguer les notes de l'arpège des notes de passage, mais
: les notes des grands intervalles font partie de l'accord.
Ainsi, sur le premier temps de la première mesure (la basse est un ''mi''♭), on a une sixte descendante ''sol''-''si''♭ et, à la fin du temps, une tierce descendante ''sol''-''mi''♭. L'accord est donc ''mi''♭-''sol''-''si''♭, c'est un accord de quinte (accord parfait à l'état fondamental). À la fin du premier temps, le basson joue un ''do'', c'est donc une note étrangère.
Sur le second temps de la première mesure, le basson joue une tierce ascendante ''fa''-''la''♭, la première note est la basse de l'accord et la seconde une des notes de l'accord. Le hautbois commence par une sixte descendante ''la''♭-''do'', l'accord est donc ''fa''-''la''♭-''do'', un accord de quinte (accord parfait à l'état fondamental). Le ''do'' du basson la fin du premier temps est donc une anticipation.
Les autres notes étrangères de la première mesure sont des notes de passage.
Mais il faut faire attention : en suivant ce principe, sur les premiers temps des deuxième et troisième mesure, nous aurions des accords de septième d'espèce (puisque la septième est majeure). Or, on ne trouve pas, ou alors exceptionnellement, d'accord de septième d'espèce dans le baroque, mais quasi exclusivement des accords de septième de dominante. Donc au début de la deuxième mesure, le ''la''♮ est une appoggiature du ''si''♭, l'accord est donc ''si''♭-''ré''-''fa'', un asscord de quinte. De même, au début de la troisième mesure, le ''sol'' est une appoggiature du ''la''♭.
Il faut donc se méfier d'une analyse purement « mathématique ». Il faut s'attacher à ressentir la musique, et à connaître les styles, pour faire une analyse pertinente.
Ci-dessous, nous avons grisé les notes étrangères.
[[Fichier:Sonate hautbois basson heinichen 2e mvt mes49 analyse.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen. Analyse de la progression harmonique.]]
Le chiffrage jazz équivalent est :
: | E♭ F– | B♭<sup>Δ</sup> E♭ | A♭<sup>Δ</sup> D– | G …
Nous remarquons une progression assez régulière :
: ''mi''♭ ↗[2<sup>de</sup>] ''fa'' | ↘[5<sup>te</sup>] ''si''♭ ↗[4<sup>te</sup>] ''mi''♭ | ↘[5<sup>te</sup>] ''la''♭ ↗[4<sup>te</sup>] ''ré'' | ↘[5<sup>te</sup>] ''sol''
Le ''mi''♭ est le degré {{Times New Roman|III}} de la tonalité principale (''do'' mineur), c'est donc une tonique faible ; il « joue le même rôle » qu'un ''do''. S'il y avait eu un accord de ''do'' au début de l'extrait, on aurait eu une progression parfaitement régulière ↗[4<sup>te</sup>] ↘[5<sup>te</sup>].
Nous avons les modulations suivantes :
* mesure 49 : ''do'' mineur naturel (le ''si''♭ n'est pas une sensible) avec un accord sur “{{Times New Roman|I}}” (tonique faible, {{Times New Roman|III}}, pour la première analyse, ou bien tonique forte, {{Times New Roman|I}}, pour la seconde) suivi d'un accord sur {{Times New Roman|IV}} ;
* mesure 50 : ''si''♭ majeur avec un accord sur {{Times New Roman|I}} suivi d'un accord sur {{Times New Roman|IV}} ;
* mesure 51 : ''la''♭ majeur avec un accord sur {{Times New Roman|I}}, et emprunt à ''do'' majeur avec un accord sur {{Times New Roman|II}} ({{Times New Roman|IV}} faible).
On a donc une marche harmonique {{Times New Roman|I}} → {{Times New Roman|IV}} qui descend d'une seconde majeure (un ton) à chaque mesure (''do'' → ''si''♭ → ''la''♭), avec une exception sur la dernière mesure (modulation en cours de mesure et descente d'une seconde mineure au lieu de majeure).
Ce passage est donc construit sur une régularité, une règle qui crée un effet d'attente — enchaînement {{Times New Roman|I}}<sup>5</sup> → {{Times New Roman|IV}}<sup>5</sup> avec une marche harmonique d'une seconde majeure descendante —, et des « surprises », des exceptions au début — ce n'est pas un accord {{Times New Roman|I}}<sup>5</sup> mais un accord {{Times New Roman|III}}<sup>5</sup> — et à la fin — modulation en milieu de mesure et dernière descente d'une seconde mineure (½t ''la''♭ → ''sol'').
L'extrait ne permet pas de le deviner, mais la mesure 52 est un retour en ''do'' mineur, avec donc une modulation sur la dominante (accord de ''sol''<sup>7</sup><sub>+</sub>, G<sup>7</sup>).
{{boîte déroulante/fin}}
== Progression d'accords ==
Comme pour la mélodie, la succession des accords dans un morceau, la progression d'accords, suit des règles. Et comme pour la mélodie, les règles diffèrent d'un style musical à l'autre et la créativité consiste à parfois ne pas suivre ces règles. Et comme pour la mélodie, on part d'un ensemble de notes organisé, d'une gamme caractéristique d'une tonalité, d'un mode.
Les accords les plus utilisés pour une tonalité donnée sont les accords dont la fondamentale sont les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la tonalité, en particulier la triade {{Times New Roman|I}}, appelée « accord parfait » ou « accord de tonique », et l'accord de septième {{Times New Roman|V}}, appelé « septième de dominante ».
Le fait d'avoir une progression d'accords qui se répète permet de structurer un morceau. Pour les morceaux courts, il participe au plaisir de l'écoute et facilite la mémorisation (par exemple le découpage couplet-refrain d'une chanson). Sur les morceaux longs, une trop grande régularité peut introduire de la lassitude, les longs morceaux sont souvent découpés en parties présentant chacune une progression régulière. Le fait d'avoir une progression régulière permet la pratique de l'improvisation : cadence en musique classique, solo en jazz et blues.
; Note
: Le terme « cadence » désigne plusieurs choses différentes, et notamment en harmonie :
:* une partie improvisée dans un opéra ou un concerto, sens utilisé ci-dessus ;
:* une progression d'accords pour ponctuer un morceau et en particulier pour le conclure, sens utilisé dans la section suivante.
=== Accords peu utilisés ===
En mode mineur, l'accord de quinte augmentée {{Times New Roman|III<sup>+5</sup>}} est très peu utilisé. C'est un accord dissonant ; il intervient en général comme appogiature de l'accord de tonique (par exemple en ''la'' mineur : {{Times New Roman|III<sup>+5</sup>}} ''do'' - ''mi'' - ''sol''♯ → {{Times New Roman|I<sup>6</sup>}} ''do'' - ''mi'' - ''la''), ou de l'accord de dominante ({{Times New Roman|III<sup>6</sup><sub>+3</sub>}} ''mi'' - ''sol''♯ - ''do'' → {{Times New Roman|V<sup>5</sup>}} ''mi'' - ''sol''♯ - ''si''). Il peut être aussi utilisé comme préparation à l'accord de sous-dominante (enchaînement {{Times New Roman|III}} → {{Times New Roman|IV}}). Par ailleurs, il a une constitution symétrique — c'est l'empilement de deux tierces majeures — et ses renversements ont les mêmes intervalles à l'enharmonie près (quinte augmentée/sixte mineure, tierce majeure/quarte diminuée). De ce fait, un même accord est commun, par renversement et à l'enharmonie près, à trois tonalités : le premier renversement de l'accord ''do'' - ''mi'' - ''sol''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur) est enharmonique à ''mi'' - ''sol''♯ - ''si''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''do''♯ mineur) ; le second renversement est enharmonique à ''la''♭ - ''do'' - ''mi'' ({{Times New Roman|III}}<sup>e</sup> degré de ''fa'' mineur).
=== Accords très utilisés ===
Les trois accords les plus utilisés sont les accords de tonique (degré {{Times New Roman|I}}), de sous-dominante ({{Times New Roman|IV}}) et de dominante ({{Times New Roman|V}}). Ils interviennent en particulier en fin de phrase, dans les cadences. L'accord de dominante sert souvent à introduire une modulation : la modulation commence sur l'accord de dominante de la nouvelle tonalité. On note que l'accord de sous-dominante est situé une quinte juste en dessous de la tonique, les accords de dominante et de sous-dominante sont donc symétriques.
En jazz, on utilise également très fréquemment l'accord de la sus-tonique (degré {{Times New Roman|II}}), souvent dans des progressions {{Times New Roman|II}} - {{Times New Roman|V}} (- {{Times New Roman|I}}). Rappelons que l'accord de sus-tonique a la fonction de sous-dominante.
=== Cadences et ''turnaround'' ===
Le terme « cadence » provient de l'italien ''cadenza'' et désigne la « chute », la fin d'un morceau ou d'une phrase musicale.
On distingue deux types de cadences :
* les cadences conclusive, qui créent une sensation de complétude ;
* les cadences suspensives, qui crèent une sensation d'attente.
==== Cadence parfaite ====
[[Fichier:Au clair de le lune cadence parfaite.midi|thumb|''Au clair de la lune'', harmonisé avec une cadence parfaite (italienne).]]
[[Fichier:Au clair de le lune mineur cadence parfaite.midi|thumb|''Idem'' mais en mode mineur harmonique.]]
La cadence parfaite est l'enchaînement de l'accord de dominante suivi de l'accord parfait : {{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}}, les deux accord étant à l'état fondamental. Elle donne une impression de stabilité et est donc très souvent utilisée pour conclure un morceau. C'est une cadence conclusive.
On peut aussi utiliser l'accord de septième de dominante, la dissonance introduisant une tension résolue par l'accord parfait : {{Times New Roman|V<sup>7</sup><sub>+</sub> - I<sup>5</sup>}}.
Elle est souvent précédée de l'accord construit sur le IV<sup>e</sup> degré, appelé « accord de préparation », pour former la cadence italienne : {{Times New Roman|IV<sup>5</sup> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}}.
Elle est également souvent précédée du second renversement de l'accord de tonique, qui est alors appelé « appoggiature de la cadence » : {{Times New Roman|I<sup>6</sup><sub>4</sub> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}} (on remarque que les accords {{Times New Roman|I}}<sup>6</sup><sub>4</sub> et {{Times New Roman|V}}<sup>5</sup> ont la basse en commun, et que l'on peut passer de l'un à l'autre par un mouvement conjoint sur les autres notes).
{{clear}}
==== Demi-cadence ====
[[Fichier:Au clair de le lune demi cadence.midi|thumb|''Au clair de la lune'', harmonisé avec une demi-cadence.]]
Une demi-cadence est une phrase ou un morceau se concluant sur l'accord construit sur le cinquième degré. Il provoque une sensation d'attente, de suspens. Il s'agit en général d'une succession {{Times New Roman|II - V}} ou {{Times New Roman|IV - V}}. C'est une cadence suspensive. On uilise rarement un accord de septième de dominante.
{{clear}}
==== Cadence rompue ou évitée ====
La cadence rompue, ou cadence évitée, est succession d'un accord de dominante et d'un accord de sus-dominante, {{Times New Roman|V}} - {{Times New Roman|VI}}. C'est une cadence suspensive.
==== Cadence imparfaite ====
Une cadence imparfaite est une cadence {{Times New Roman|V - I}}, comme la cadence parfaite, mais dont au moins un des deux accords est dans un état renversé.
==== Cadence plagale ====
La cadence plagale — du grec ''plagios'', oblique, en biais — est la succession de l'accord construit sur le quatrième degré, suivi de l'accord parfait : {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}. Elle peut être utilisée après une cadence parfaite ({{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}} - {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}). Elle donne un caractère solennel, voire religieux — elle est parfois appelée « cadence amen » —, elle a un côté antique qui rappelle la musique modale et médiévale<ref>{{lien web |url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/la-cadence-amen-ou-comment-se-dire-adieu-7191921 |titre=La cadence « Amen » ou comment se dire adieu |auteur=Max Dozolme (MAXXI Classique) |site=France Musique |date=2025-04-25 |consulté le=2025-04-25}}.</ref>.
C'est une cadence conclusive.
==== {{lang|en|Turnaround}} ====
[[Fichier:Au clair de le lune turnaround.midi|thumb|Au clair de la lune, harmonisé en style jazz : accords de 7{{e}}, anatole suivie d'un ''{{lang|en|turnaround}}'' ii-V-I.]]
Le terme ''{{lang|en|turnaround}}'' signifie revirement, retournement. C'est une succession d'accords que fait la transition entre deux parties, en créant une tension-résolution. Le ''{{lang|en|turnaround}}'' le plus courant est la succession {{Times New Roman|II - V - I}}.
On utilise également fréquemment l'anatole : {{Times New Roman|I - VI - II - V}}.
{| class="wikitable"
|+ Progressions typiques d'accords dans une tonalité majeure
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Cadence<br />parfaite<br /> {{Times New Roman|V - I}}
! scope="col" | Cadence<br />italienne<br />{{Times New Roman|IV - V - I}}
! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou IV - V}}
! scope="col" | Cadence<br />plagale<br />{{Times New Roman|IV - I}}
! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}}
! scope="col" | Anatole<br />{{Times New Roman|I - vi - ii - V}}
|-
|''Do'' majeur || || G - C || F - G - C || Dm - G ou F - G || F - C || Dm - G - C || C - Am - Dm - G
|-
|''Sol'' majeur || ''fa''♯ || D - G || C - D - G || Am - D ou C - D || C - G || Am - D - G || G - Em - Am - D
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || A - D || G - A - D || Em - A ou G - A || G - D || Em - A - D || D - Bm - Em - A
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || E - A || D - E - A || Bm - E ou D - E || D - A || Bm - E - A || A - F♯m - B - E
|-
| ''Fa'' majeur || ''si''♭ || C - F || B♭ - C - F || Gm - C ou B♭ - C || B♭ - F || Gm - C - F || F - Dm - Gm - C
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || F - B♭ || E♭ - F - B♭ || Cm - F ou E♭ - F || E♭ - B♭ || Cm - F - B♭ || B♭ - Gm - Cm - F
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || B♭ - E♭ || A♭ - B♭ - E♭ || Fm - B♭ ou A♭ - B♭ || A♭ - E♭ || Fm - B♭ - E♭ || Gm - Cm - Fm - B♭
|}
{| class="wikitable"
|+ Progressions typiques d'accords dans une tonalité mineure
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Cadence<br />parfaite<br />{{Times New Roman|V - i}}
! scope="col" | Cadence<br />italienne<br />{{Times New Roman|iv - V - i}}
! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou iv - V}}
! scope="col" | Cadence<br />plagale<br />{{Times New Roman|iv - i}}
! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}}
! scope="col" | Anatole<br />{{Times New Roman|i - VI - ii - V}}
|-
| ''La'' mineur<br />harmonique || || E - Am || Dm - E - Am || B° - E ou Dm - E || Dm - Am || B° - E - Am || Am - F - B° - E
|-
| ''Mi'' mineur<br />harmonique || ''fa''♯ || B - Em || Am - B - Em || F♯° - B ou Am - B || Am - Em || F♯° - B - Em || Em - C - F♯° - B
|-
| ''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || F♯ - Bm || Em - F♯ - Bm || C♯° - F♯ ou Em - F♯ || Em - Bm || C♯° - F♯ - Bm || Bm - G - C♯° - F♯
|-
| ''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || C♯ - F♯m || Bm - C♯ - F♯m || G♯° - C♯ ou Bm - C♯ || Bm - F♯m || G♯° - C♯ - F♯m || A+ - D - G♯° - C♯
|-
| ''Ré'' mineur<br />harmonique || ''si''♭ || A - Dm || Gm - A - Dm || E° - A ou Gm - A || Gm - Dm || E° - A - Dm || Dm - B♭ - E° - A
|-
| ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || D - Gm || Cm - D - Gm || A° - D ou Cm - D || Cm - Gm|| A° - D - Gm || Gm - E♭ - A° - D
|-
| ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || G - Cm || Fm - G - Cm || D° - G ou Fm - G || Fm - Dm || D° - G - Cm || Cm - A♭ - D° - G
|}
==== Exemple : ''La Mer'' ====
: {{lien web
| url = https://www.youtube.com/watch?v=PXQh9jTwwoA
| titre = Charles Trenet - La mer (Officiel) [Live Version]
| site = YouTube
| auteur = Charles Trenet
| consulté le = 2020-12-24
}}
Le début de ''La Mer'' (Charles Trenet, 1946) est en ''do'' majeur et est harmonisé par l'anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (C - Am - Dm - G<sup>7</sup>) sur deux mesures, jouée deux fois ({{Times New Roman|1=<nowiki>|I-vi|ii-V</nowiki><sup>7</sup><nowiki>|</nowiki>}} × 2). Viennent des variations avec les progressions {{Times New Roman|I-III-vi-V<sup>7</sup>}} (C - E - Am - G<sup>7</sup>) puis la « progression ’50s » (voir plus bas) {{Times New Roman|I-vi-IV-VI<sup>7</sup>}} (C - Am - F - A<sup>7</sup>, on remarque que {{Times New Roman|IV}}/F est le relatif majeur du {{Times New Roman|ii}}/Dm de l'anatole), jouées chacune une fois sur deux mesure ; puis cette première partie se conclut par une demie cadence {{Times New Roman|ii-V<sup>7</sup>}} sur une mesure puis une dernière anatole sur trois mesures ({{Times New Roman|1=<nowiki>|I-vi|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}}). Cela constitue une première partie « A » sur douze mesures qui se termine par une demi-cadence ({{Times New Roman|ii-V<sup>7</sup>}}) qui appelle une suite. Cette partie A est jouée une deuxième fois mais la fin est modifiée pour la transition : les deux dernières mesures {{Times New Roman|<nowiki>|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}} deviennent {{Times New Roman|<nowiki>|ii-V</nowiki><sup>7</sup><nowiki>|I|</nowiki>}} (|Dm-G7|C|), cette partie « A’ » se conclut donc par une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}).
Le morceau passe ensuite en tonalité de ''mi'' majeur, donc une tierce au dessus de ''do'' majeur, sur six mesures. Cette partie utilise une progression ’50s {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (E - C♯m - A - B<sup>7</sup>), qui est rappelons-le une variation de l'anatole, l'accord {{Times New Roman|ii}} (Fm) étant remplacé par son relatif majeur {{Times New Roman|IV}} (A). Cette anatole modifiée est jouée deux fois puis la partie en ''mi'' majeur se conclut par l'accord parfait {{Times New Roman|I}} joué sur deux mesures (|E|E|), on a donc, avec la mesure précédente, avec une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}).
Suivent ensuite six mesures en ''sol'' majeur, donc à nouveau une tierce au dessus de ''mi'' majeur. Elle comporte une progression {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (G - Em - C - D<sup>7</sup>), donc anatole avec substitution du {{Times New Roman|ii}}/Am par son relatif majeur {{Times New Roman|VI}}/C (progression ’50s), puis une anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (G - Em - Am - D<sup>7</sup>) et deux mesure sur la tonique {{Times New Roman|I-I<sup>7</sup>}} (G - G<sup>7</sup>), formant à nouveau une cadence parfaite. La fin sur un accord de septième, dissonant, appelle une suite.
Cette partie « B » de douze mesures comporte donc deux parties similaires « B1 » et « B2 » qui forment une marche harmonique (montée d'une tierce).
Le morceau se conclut par une reprise de la partie « A’ » et se termine donc par une cadence parfaite.
Nous avons une structure A-A’-B-A’ sur 48 mesures, proche la forme AABA étudiée plus loin.
Donc ''La Mer'' est un morceau structuré autour de l'anatole avec des variations (progression ’50s, substitution du {{Times New Roman|ii}} par son relatif majeur {{Times New Roman|IV}}) et comportant une marche harmonique dans sa troisième partie. Les parties se concluent par des ''{{lang|en|turnarounds}}'' sous la forme d'une cadence parfaite ou, pour la partie A, par une demi-cadence.
{| border="1" rules="rows" frame="hsides"
|+ Structure de ''La Mer''
|- align="center"
|
| colspan="12" | ''do'' majeur
|
|- align="center"
! scope="row" rowspan=2 | A
| colspan="2" | anatole
| colspan="2" | //
| colspan="2" | variation
| colspan="2" | ’50s
| ½ c.
| colspan="3" | anatole
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii}} || <nowiki>|</nowiki> {{Times New Roman|V<sup>7</sup>}} || <nowiki>|</nowiki>
|- align="center"
! scope="row" rowspan="2" | A’
| colspan="2" | anatole
| colspan="2" | //
| colspan="2" | variation
| colspan="2" | ’50s
| ½ c.
| colspan="2" | anatole
| c.p.
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki>
|- align="center"
|
| colspan="6" | B1 : ''mi'' majeur
| colspan="6" background="lightgray" | B2 : ''sol'' majeur
|
|- align="center"
! scope="row" rowspan="2" | B
| colspan="2" | ’50s
| colspan="2" | //
|colspan="2" | c.p.
| colspan="2" | ’50s
| colspan="2" | //
|colspan="2" | c.p.
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I<sup>7</sup>}} || <nowiki>|</nowiki>
|-
! scope="row" | A’
| colspan="12" |
|
|}
=== Progression blues ===
La musique blues est apparue dans les années 1860. Elle est en général bâtie sur une grille d'accords ''({{lang|en|changes}})'' immuable de douze mesures ''({{lang|en|twelve-bar blues}})''. C'est sur cet accompagnement qui se répète que s'ajoute la mélodie — chant et solo. Cette structure est typique du blues et se retrouve dans ses dérivés comme le rock 'n' roll.
Le rythme est toujours un rythme ternaire syncopé ''({{lang|en|shuffle, swing, groove}}, ''notes inégales'')'' : la mesure est à quatre temps, mais la noire est divisée en noire-croche en triolet, ou encore triolet de croche en appuyant la première et la troisième.
La mélodie se construit en général sur une gamme blues de six degrés (gamme pentatonique mineure avec une quarte augmentée), mais bien que la gamme soit mineure, l'harmonie est construite sur la gamme majeure homonyme : un blues en ''fa'' a une mélodie sur la gamme de ''fa'' mineur, mais une harmonie sur la gamme de ''fa'' majeur. La grille d'accord comporte les accords construits sur les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la gamme majeure homonyme. Les accords sont souvent des accords de septième (donc avec une tierce majeure et une septième mineure), il ne s'agit donc pas d'une harmonisation de gamme diatonique (puisque la septième est majeure sur l'accord de tonique).
Par exemple, pour un blues en ''do'' :
* accord parfait de do majeur, C ({{Times New Roman|I}}<sup>er</sup> degré) ;
* accord parfait de fa majeur, F ({{Times New Roman|IV}}<sup>e</sup> degré) ;
* accord parfait de sol majeur, G ({{Times New Roman|V}}<sup>e</sup> degré).
Il existe quelques morceaux harmonisés avec des accords mineurs, comme par exemple ''As the Years Go Passing By'' d'Albert King (Duje Records, 1959).
La progression blues est organisée en trois blocs de quatre mesures ayant les fonctions suivantes (voir ci-dessus ''[[#Harmonie fonctionnelle|Harmonie fonctionnelle]]'') :
* quatre mesures toniques ;
* quatre mesures sous-dominantes ;
* quatre mesures dominantes.
La forme la plus simple, que Jeff Gardner appelle « forme A », est la suivante :
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, forme A
|-
! scope="row" | Tonique
| width="50px" | I
| width="50px" | I
| width="50px" | I
| width="50px" | I
|-
! scope="row" | Sous-domminante
| width="50px" | IV
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
! scope="row" | Dominante
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | V
|}
La progression {{Times New Roman|I-V}} des deux dernières mesures forment le ''{{lang|en|turnaround}}'', la demie cadence qui lance le cycle suivant. Nous présentons ci-dessous un exemple typique de ligne de basse ''({{lang|en|walking bass}})'' pour le ''{{lang|en|turnaround}}'' d'un blues en ''la'' :
[[Fichier:Turnaround classique blues en la.svg|Exemple typique de ligne de basse pour un ''turnaround'' de blues en ''la''.]]
[[Fichier:Blues mi harmonie elementaire.midi|thumb|Blues en ''mi'', harmonisé de manière élémentaire avec une ''{{lang|en|walking bass}}''.]]
Vous pouvez écouter ci-contre une harmonisation typique d'un blues en ''mi''. Les accords sont exécutés par une basse marchante ''({{lang|en|walking bass}})'', qui joue une arpège sur la triade avec l'ajout d'une sixte majeure et d'une septième mineure, et par une guitare qui joue un accord de puissance ''({{lang|en|power chord}})'', qui n'est composé que de la fondamentale et de la quinte juste, avec une sixte en appoggiature.
La forme B s'obtient en changeant la deuxième mesure : on joue un degré {{Times New Roman|IV}} au lieu d'un degré {{Times New Roman|I}}. La progression {{Times New Roman|I-IV}} sur les deux premières mesures est appelé ''{{lang|en|quick change}}''.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, forme B
|-
| width="50px" | I
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
| width="50px" | IV
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | V
|}
Par exemple, ''Sweet Home Chicago'' (Robert Johnson, 1936) est un blues en ''fa'' ; sa grille d'accords, aux variations près, suit une forme B :
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression de ''Sweet Home Chicago''
|-
| width="50px" | F
| width="50px" | B♭
| width="50px" | F
| width="50px" | F
|-
| width="50px" | B♭
| width="50px" | B♭
| width="50px" | F
| width="50px" | F
|-
| width="50px" | C7
| width="50px" | B♭7
| width="50px" | F7
| width="50px" | C7
|}
: Écouter {{lien web
| url =https://www.youtube.com/watch?v=dkftesK2dck
| titre = Robert Johnson "Sweet Home Chicago"
| auteur = Michal Angel
| site = YouTube
| date = 2007-12-09 | consulté le = 2020-12-17
}}
Les formes C et D s'obtiennent à partir des formes A et B en changeant le dernier accord par un accord sur le degré {{Times New Roman|I}}, ce qui forme une cadence plagale.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, formes C et D
|-
| colspan="4" | …
|-
| colspan="4" | …
|-
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|}
L'harmonie peut être enrichie, notamment en jazz. Voici par exemple une grille du blues souvent utilisés en bebop.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Exemple de progression de blues bebop sur une base de forme B
|-
| width="60px" | I<sup>7</sup>
| width="60px" | IV<sup>7</sup>
| width="60px" | I<sup>7</sup>
| width="60px" | V–<sup>7</sup> <nowiki>|</nowiki> I<sup>7</sup>
|-
| width="60px" | IV<sup>7</sup>
| width="60px" | IV<sup>7</sup>
| width="60px" | I<sup>7</sup>
| width="60px" | VI<sup>7 ♯9 ♭13</sup>
|-
| width="60px" | II–<sup>7</sup>
| width="60px" | V<sup>7</sup>
| width="60px" | V<sup>7</sup> <nowiki>|</nowiki> IV<sup>7</sup>
| width="60px" | II–<sup>7</sup> <nowiki>|</nowiki> V<sup>7</sup>
|}
On peut aussi trouver des blues sur huit mesures, sur seize mesures comme ''Watermelon Man'' de Herbie Hancock (album ''Takin' Off'', Blue Note, 1962) ou ''Let's Dance'' de Jim Lee (interprété par Chris Montez, Monogram, 1962)
* {{lien web
|url= https://www.dailymotion.com/video/x5iduwo
|titre=Herbie Hancock - Watermelon Man (1962)
|auteur=theUnforgettablesTv
|site=Dailymotion
|date=2003 |consulté le=2021-02-09
}}
* {{lien web
|url=https://www.youtube.com/watch?v=6JXshurYONc
|titre=Let's Dance
|auteur=Chris Montez
|site=YouTube
|date=2016-08-06 |consulté le=2021-02-09
}}
À l'inverse, certains blues peuvent avoir une structure plus simple que les douze mesure ; par exemple ''Hoochie Coochie Man'' de Willie Dixon (interprété par Muddy Waters sous le titre ''Mannish Boy'', Chicago Blues, 1954) est construit sur un seul accord répété tout le long de la chanson.
* {{lien web
|url=https://www.dailymotion.com/video/x5iduwo
|titre=Muddy Waters - Hoochie Coochie Man
|auteur=Muddy Waters
|site=Dailymotion
|date=2012 | consulté le=2021-02-09
}}
=== Cadence andalouse ===
La cadence andalouse est une progression de quatre accords, descendant par mouvement conjoint :
* en mode de ''mi'' (mode phrygien) : {{Times New Roman|IV}} - {{Times New Roman|III}} - {{Times New Roman|II}} - {{Times New Roman|I}} ;<br />par exemple en ''mi'' phrygien : Am - G - F - E ; en ''do'' phrygien : Fm - E♭ - D♭ - C ;<br />on notera que le degré {{Times New Roman|III}} est diésé dans l'accord final (ou bécarre s'il est bémol dans la tonalité) ;
* en mode mineur : {{Times New Roman|I}} - {{Times New Roman|VII}} - {{Times New Roman|VI}} - {{Times New Roman|V}} ;<br />par exemple en ''la'' mineur : Am - G - F - E ; en ''do'' mineur : Cm - B♭ - A♭ - m ;<br />comme précédemment, on notera que le degré {{Times New Roman|VII}} est diésé dans l'accord final.
=== Progressions selon le cercle des quintes ===
[[Fichier:Cercle quintes degres tonalite majeure.svg|vignette|Cercle des quinte justes (parcouru dans le sens des aiguilles d'une montre) des degrés d'une tonalité majeure.]]
La progression {{Times New Roman|V-I}} est la cadence parfaite, mais on peut aussi l'employer au milieu d'un morceau. Cette progression étant courte, sa répétition crée de la lassitude ; on peut la compléter par d'autres accords séparés d'une quinte juste, en suivant le « cercle des quintes » : {{Times New Roman|I-V-IX}}, la neuvième étant enharmonique de la seconde, on obtient {{Times New Roman|I-V-II}}.
On peut continuer de décrire le cercle des quintes : {{Times New Roman|I-V-II-VI}}, on obtient l'anatole dans le désordre ; on peut à l'inverse étendre les quintes vers la gauche, {{Times New Roman|IV-I-V-II-VI}}.
En musique populaire, on trouve fréquemment une progression fondée sur les accord {{Times New Roman|I}}, {{Times New Roman|IV}}, {{Times New Roman|V}} et {{Times New Roman|VI}}, popularisée dans les années 1950. La « progression années 1950 », « progression ''{{lang|en|fifties ('50)}}'' » ''({{lang|en|'50s progression}})'' est dans l'ordre {{Times New Roman|I-VI-IV-V}}. On trouve aussi cette progression en musique classique. Si la tonalité est majeure, la triade sur la sus-dominante est mineure, les autres sont majeures, on notera donc souvent {{Times New Roman|I-vi-IV-V}}. On peut avoir des permutations circulaires (le dernier accord venant au début, ou vice-versa) : {{Times New Roman|vi-IV-V-I}}, {{Times New Roman|IV-V-I-vi}} et {{Times New Roman|V-I-vi-IV}}.
{| class="wikitable"
|+ Accords selon la tonalité
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" style="font-family:Times New Roman" | I
! scope="col" style="font-family:Times New Roman" | IV
! scope="col" style="font-family:Times New Roman" | V
! scope="col" style="font-family:Times New Roman" | vi
|-
|''Do'' majeur || || C || F || G || Am
|-
|''Sol'' majeur || ''fa''♯ || G || C || D || Em
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || D || G || A || Bm
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A || D || E || F♯m
|-
| ''Fa'' majeur || ''si''♭ || F || B♭ || C || Dm
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭ || E♭ || F || Gm
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭ || A♭ || B♭ || Cm
|}
Par exemple, en tonalité de ''do'' majeur, la progression {{Times New Roman|I-vi-IV-V}} sera C-Am-F-G.
Il existe d'autres progressions utilisant ces accords mais dans un autre ordre, typiquement {{Times New Roman|I–IV–vi–V}} ou une de ses permutations circulaires : {{Times New Roman|IV–vi–V-I}}, {{Times New Roman|vi–V-I-IV}} ou {{Times New Roman|V-I-IV-vi}}. Ou dans un autre ordre.
PV Nova l'illustre dans plusieurs de ses « expériences » dans la version {{Times New Roman|vi-V-IV-I}}, soit Am-G-F-C, ou encore {{Times New Roman|vi-IV-I-V}}, soit Am-F-C-G :
: {{lien web
| url = https://www.youtube.com/watch?v=w08LeZGbXq4
| titre = Expérience n<sup>o</sup> 6 — La Happy Pop
| auteur = PV Nova
| site = YouTube
| date = 2011-08-20 | consulté le = 2020-12-13
}}
et cela devient un gag récurrent avec son « chapeau des accords magiques qu'on nous ressort à toutes les sauces »
: {{lien web
| url = https://www.youtube.com/watch?v=VMY_vc4nZAU
| titre = Expérience n<sup>o</sup> 14 — La Soupe dou Brasil
| auteur = PV Nova
| site = YouTube
| date = 2012-10-03 | consulté le = 2020-12-17
}}
Cette récurrence est également parodiée par le groupe The Axis of Awesome avec ses « chansons à quatre accords » ''({{lang|en|four-chords song}})'', dans une sketch où ils mêlent 47 chansons en utilisant l'ordre {{Times New Roman|I-V-vi-IV}} :
: {{lien web
| url = https://www.youtube.com/watch?v=oOlDewpCfZQ
| titre = 4 Chords | Music Videos | The Axis Of Awesome
| auteur = The Axis of Awesome
| site = YouTube
| date = 2011-07-20 | consulté le = 2020-12-17
}}
{{boîte déroulante/début|titre=Chansons mêlées dans le sketch}}
# Journey : ''Don't Stop Believing'' ;
# James Blunt : ''You're Beautiful'' ;
# Black Eyed Peas : ''Where Is the Love'' ;
# Alphaville : ''Forever Young'' ;
# Jason Mraz : ''I'm Yours'' ;
# Train : ''Hey Soul Sister'' ;
# The Calling : ''Wherever You Will Go'' ;
# Elton John : ''Can You Feel The Love Tonight'' (''Le Roi lion'') ;
# Akon : ''Don't Matter'' ;
# John Denver : ''Take Me Home, Country Roads'' ;
# Lady Gaga : ''Paparazzi'' ;
# U2 : ''With Or Without You'' ;
# The Last Goodnight : ''Pictures of You'' ;
# Maroon Five : ''She Will Be Loved'' ;
# The Beatles : ''Let It Be'' ;
# Bob Marley : ''No Woman No Cry'' ;
# Marcy Playground : ''Sex and Candy'' ;
# Men At Work : ''Land Down Under'' ;
# thème de ''America's Funniest Home Videos'' (équivalent des émissions ''Vidéo Gag'' et ''Drôle de vidéo'') ;
# Jack Johnson : ''Taylor'' ;
# Spice Girls : ''Two Become One'' ;
# A Ha : ''Take On Me'' ;
# Green Day : ''When I Come Around'' ;
# Eagle Eye Cherry : ''Save Tonight'' ;
# Toto : ''Africa'' ;
# Beyonce : ''If I Were A Boy'' ;
# Kelly Clarkson : ''Behind These Hazel Eyes'' ;
# Jason DeRulo : ''In My Head'' ;
# The Smashing Pumpkins : ''Bullet With Butterfly Wings'' ;
# Joan Osborne : ''One Of Us'' ;
# Avril Lavigne : ''Complicated'' ;
# The Offspring : ''Self Esteem'' ;
# The Offspring : ''You're Gonna Go Far Kid'' ;
# Akon : ''Beautiful'' ;
# Timberland featuring OneRepublic : ''Apologize'' ;
# Eminem featuring Rihanna : ''Love the Way You Lie'' ;
# Bon Jovi : ''It's My Life'' ;
# Lady Gaga : ''Pokerface'' ;
# Aqua : ''Barbie Girl'' ;
# Red Hot Chili Peppers : ''Otherside'' ;
# The Gregory Brothers : ''Double Rainbow'' ;
# MGMT : ''Kids'' ;
# Andrea Bocelli : ''Time To Say Goodbye'' ;
# Robert Burns : ''Auld Lang Syne'' ;
# Five for fighting : ''Superman'' ;
# The Axis of Awesome : ''Birdplane'' ;
# Missy Higgins : ''Scar''.
{{boîte déroulante/fin}}
Vous pouvez par exemple jouer les accords C-G-Am-F ({{Times New Roman|I-V-vi-IV}}) et chanter dessus ''{{lang|en|Let It Be}}'' (Paul McCartney, The Beattles, 1970) ou ''Libérée, délivrée'' (Robert Lopez, ''La Reine des neiges'', 2013).
La progression {{Times New Roman|I-V-vi-IV}} est considérée comme « optimiste » tandis que sa variante {{Times New Roman|iv-IV-I-V}} est considérée comme « pessimiste ».
On peut voir la progression {{Times New Roman|I-vi-IV-V}} comme une variante de l'anatole {{Times New Roman|I-vi-ii-V}}, obtenue en remplaçant l'accord de sustonique {{Times New Roman|ii}} par l'accord de sous-dominante {{Times New Roman|IV}} (son relatif majeur, et degré ayant la même fonction).
==== Exemples de progression selon le cercle des quintes en musique classique ====
[[Fichier:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.midi|vignette|Dietrich Buxtehude, Psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]]
Cette progression selon la cercle des quintes, sous la forme {{Times New Roman|I-vi-IV-V}}, apparaît déjà au {{pc|xvii}}<sup>e</sup> siècle dans le psaume 42 ''Quem ad modum desiderat cervis'' (BuxVW92) de Dietrich Buxtehude (1637-1707). Le morceau est en ''fa'' majeur, la progression d'accords est donc F-Dm-B♭-C.
: {{lien web
| url = https://www.youtube.com/watch?v=8FmV9l1RqSg
| titre = D. Buxtehude - Quemadmodum desiderat cervus, BuxWV 92
| auteur = Longobardo
| site = YouTube
| date = 2013-04-06 | consulté la = 2021-01-01
}}
[[File:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.svg|vignette|450x450px|center|Dietrich Buxtehude, psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]]
{{clear}}
[[Fichier:JSBach BWV140 cantate 4 mesures.midi|vignette|J.-S. Bach, cantate BWV140, quatre premières mesures.]]
On la trouve également dans l'ouverture de la cantate ''{{lang|de|Wachet auf, ruft uns die Stimme}}'' de Jean-Sébastien Bach (BWV140, 1731). Le morceau est en ''mi''♭ majeur, la progression d'accords est donc E♭-Cm-A♭<sup>6</sup>-B♭.
[[Fichier:JSBach BWV140 cantate 4 mesures.svg|vignette|center|J.-S. Bach, cantate BWV140, quatre premières mesures.|alt=|517x517px]]
{{clear}}
[[Fichier:Mozart K310 Sonate8 mesures 45 a 49.midi|vignette|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]]
La même progression est utilisée par Mozart, par exemple dans le premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778), la progression d'accords est C-Am-F-G qui correspond à la progression {{Times New Roman|III-i-VI-VII}} de ''la'' mineur, mais à la progression {{Times New Roman|I-vi-IV-V}} de la gamme relative, ''do'' majeur .
[[Fichier:Mozart K310 Sonate8 mesures 45 a 49.svg|vignette|center|500px|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]]
=== Substitution tritonique ===
Un des accords les plus utilisés est donc l'accord de septième de dominante, {{Times New Roman|V<sup>7</sup><sub>+</sub>}} qui contient les degrés {{Times New Roman|V}}, {{Times New Roman|VII}}, {{Times New Roman|II}} ({{Times New Roman|IX}}) et {{Times New Roman|IV}}({{Times New Roman|XI}}) ; par exemple, en tonalité de ''do'' majeur, l'accord de ''sol'' septième (G<sup>7</sup>) contient les notes ''sol''-''si''-''ré''-''fa''. Si l'on prend l'accord dont la fondamentale est trois tons (triton) au-dessus ou en dessous — l'octave contenant six tons, on arrive sur la même note —, {{Times New Roman|♭II<sup>7</sup>}}, ici ''ré''♭ septième (D♭<sup>7</sup>), celui-ci contient les notes ''ré''♭-''fa''-''la''♭-''do''♭, cette dernière note étant l'enharmonique de ''si''. Les deux accords G<sup>7</sup> et D♭<sup>7</sup> ont donc deux notes en commun : le ''fa'' et le ''si''/''do''♭.
Il est donc fréquent en jazz de substituer l'accord {{Times New Roman|V<sup>7</sup><sub>+</sub>}} par l'accord {{Times New Roman|♭II<sup>7</sup>}}. Par exemple, la progression {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|V<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}} devient {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|♭II<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}}. C'est un procédé courant de réharmonisation (le fait de remplacer un accord par un autre dans un morceau existant).
Les six substitutions possibles sont donc : C<sup>7</sup>↔F♯<sup>7</sup> - D♭<sup>7</sup>↔G<sup>7</sup> - D<sup>7</sup>↔A♭<sup>7</sup> - E♭<sup>7</sup>↔A<sup>7</sup> - E<sup>7</sup>↔B♭<sup>7</sup> - F<sup>7</sup>↔B<sup>7</sup>.
[[Fichier:Übermäsiger Terzquartakkord.jpg|vignette|Exemple de cadence parfaite en ''do'' majeur avec substitution tritonique (sixte française).]]
Dans l'accord D♭<sup>7</sup>, si l'on remplace le ''do''♭ par son ''si'' enharmonique, on obtient un accord de sixte augmentée : ''ré''♭-''fa''-''la''♭-''si''. Cet accord est utilisé en musique classique depuis la Renaissance ; on distingue en fait trois accords de sixte augmentée :
* sixte française ''ré''♭-''fa''-''sol''-''si'' ;
* sixte allemande : ''ré''♭-''fa''-''la''♭-''si'' ;
* sixte italienne : ''ré''♭-''fa''-''si''.
Par exemple, le ''Quintuor en ''ut'' majeur'' de Franz Schubert (1828) se termine par une cadence parfaite dont l'accord de dominante est remplacé par une sixte française ''ré''♭-''fa''-''si''-''sol''-''si'' (''ré''♭ aux violoncelles, ''fa'' à l'alto, ''si''-''sol'' aux seconds violons et ''si'' au premier violon).
[[Fichier:Schubert C major Quintet ending.wav|vignette|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]]
[[Fichier:Schubert C major Quintet ending.png|vignette|center|upright=2.5|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]]
=== Autres accords de substitution ===
Substituer un accord consiste à utiliser un accord provenant d'une tonalité étrangère à la tonalité en cours. À la différence d'une modulation, la substitution est très courte et ne donne pas l'impression de changer de tonalité ; on a juste un sentiment « étrange » passager. Un court passage dans une autre tonalité est également appelée « emprunt ».
Nous avons déjà vu plusieurs méthodes de substitution :
* utilisation d'une note étrangère : une note étrangère — note de passage, appoggiature, anticipation, retard… — crée momentanément un accord hors tonalité ; en musique classique, ceci n'est pas considéré comme un accord en propre, mais en jazz, on parle « d'accord de passage » et « d'accord suspendu » ;
* utilisation d'une dominante secondaire : l'accord de dominante secondaire est hors tonalité ; le but ici est de faire une cadence parfaite, mais sur un autre degré que la tonique de la tonalité en cours ;
* la substitution tritonique, vue ci-dessus, pour remplacer un accord de septième de dominante.
Une dernière méthode consiste à remplacer un accord par un accord d'une gamme de même tonique, mais d'un autre mode ; on « emprunte » ''({{lang|en|borrow}})'' l'accord d'un autre mode. Par exemple, substituer un accord de la tonalité de ''do'' majeur par un accord de la tonalité de ''do'' mineur ou de ''do'' mode de ''mi'' (phrygien).
Donc en ''do'' majeur, on peut remplacer un accord de ''ré'' mineur septième (D<sub>m</sub><sup>7</sup>) par un accord de ''ré'' demi-diminué (D<sup>⌀</sup>, D<sub>m</sub><sup>7♭5</sup>) qui est un accord appartenant à la donalité de ''la'' mineur harmonique.
=== Forme AABA ===
La forme AABA est composée de deux progressions de huit mesures, notées A et B ; cela représente trente-deux mesures au total, on parle donc souvent en anglais de la ''{{lang|en|32-bars form}}''. C'est une forme que l'on retrouve dans de nombreuses chanson de comédies musicales de Broadway comme ''Have You Met Miss Jones'' (''{{lang|en|I'd Rather Be Right}}'', 1937), ''{{lang|en|Over the Rainbow}}'' (''Le Magicien d'Oz'', Harold Harlen, 1939), ''{{lang|en|All the Things You Are}}'' (''{{lang|en|Very Warm for may}}'', 1939).
Par exemple, la version de ''{{lang|en|Over the Rainbow}}'' chantée par Judy Garland est en ''la''♭ majeur et la progression d'accords est globalement :
* A (couplet) : A♭-Fm | Cm-A♭ | D♭ | Cm-A♭ | D♭ | D♭-F | B♭-E♭ | A♭
* B (pont) : A♭ | B♭m | Cm | D♭ | A♭ | B♭-G | Cm-G | B♭m-E♭
soit en degrés :
* A : {{Times New Roman|<nowiki>I-vi | iii-I | IV | iii-IV | IV | IV-vi | II-V | I</nowiki>}}
* B : {{Times New Roman|<nowiki>I | ii | iii | IV | I | II-VII | iii-VII | ii-V</nowiki>}}
Par rapport aux paroles de la chanson, on a
* A : couplet 1 ''« {{lang|en|Somewhere […] lullaby}} »'' ;
* A : couplet 2 ''« {{lang|en|Somewhere […] really do come true}} »'' ;
* B : pont ''« {{lang|en|Someday […] you'll find me}} »'' ;
* A : couplet 3 ''« {{lang|en|Somewhere […] oh why can't I?}} »'' ;
: {{lien web
| url = https://www.youtube.com/watch?v=1HRa4X07jdE
| titre = Judy Garland - Over The Rainbow (Subtitles)
| site = YouTube
| auteur = Overtherainbow
| consulté le = 2020-12-17
}}
Une mise en œuvre de la forme AABA couramment utilisée en jazz est la forme anatole (à le pas confondre avec la succession d'accords du même nom), en anglais ''{{lang|en|rythm changes}}'' car elle s'inspire du morceau ''{{lang|en|I Got the Rythm}}'' de George Gerschwin (''Girl Crazy'', 1930) :
* A : {{Times New Roman|I–vi–ii–V}} (succession d'accords « anatole ») ;
* B : {{Times New Roman|III<sup>7</sup>–VI<sup>7</sup>–II<sup>7</sup>–V<sup>7</sup>}} (les fondamentales forment une succession de quartes, donc parcourent le « cercle des quintes » à l'envers).
Par exemple, ''I Got the Rythm'' étant en ''ré''♭ majeur, la forme est :
* A : D♭ - B♭m - E♭m - A♭
* B : F7 - B♭7 - E♭7 - A♭7
=== Exemples ===
==== Début du Largo de la symphonie du Nouveau Monde ====
[[File:Largo nouveau monde 5 1res mesures.svg|vignette|Partition avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]]
[[File:Largo nouveau monde 5 1res mesures.midi|vignette|Fichier son avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]]
Nous avons reproduit ci-contre les cinq premières mesure du deuxième mouvement Largo de la symphonie « Du Nouveau Monde » (symphonie n<sup>o</sup> 9 d'Antonín Dvořák, 1893). Cliquez sur l'image pour l'agrandir.
Vous pouvez écouter cette partie jouée par un orchestre symphonique :
* {{lien web
|url =https://www.youtube.com/watch?v=y2Nw9r-F_yQ?t=565
|titre = Dvorak Symphony No.9 "From the New World" Karajan 1966
|site=YouTube (Seokjin Yoon)
|consulté le=2020-12-11
}} (à 9 min 25), par le Berliner Philharmoniker, dirigé par Herbert von Karajan (1966) ;
* {{lien web
|url = https://www.youtube.com/watch?v=ASlch7R1Zvo
|titre=Dvořák: Symphony №9, "From The New World" - II - Largo
|site=YouTube (diesillamusicae)
|consulté le=2020-12-11
}} : Wiener Philharmoniker, dirigé par Herbert von Karajan (1985).
{{clear}}
Cette partie fait intervenir onze instruments monodiques (ne jouant qu'une note à la fois) : des vents (trois bois, sept cuivres) et une percussion. Certains de ces instruments sont transpositeurs (les notes sur la partition ne sont pas les notes entendues). Jouées ensemble, ces onze lignes mélodiques forment des accords.
Pour étudier cette partition, nous réécrivons les parties des instruments transpositeurs en ''do'' et les parties en clef d’''ut'' en clef de ''fa''. Nous regroupons les parties en clef de ''fa'' d'un côté et les parties en clef de ''sol'' d'un autre.
{{boîte déroulante|Résultat|contenu=[[File:Largo nouveau monde 5 1res mesures transpositeurs en do.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde, en do.]]}}
Nous pouvons alors tout regrouper sous la forme d'un système de deux portées clef de ''fa'' et clef de ''sol'', comme une partition de piano.
{{boîte déroulante|Résultat|contenu=
[[Fichier:Largo nouveau monde 5 1res mesures accords.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords.]]
}}
[[Fichier:Largo nouveau monde 5 1res mesures accords.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords.]]
{{clear}}
Ensuite, nous ne gardons que la basse et les notes médium. Nous changeons éventuellement certaines notes d'octave afin de n'avoir que des superpositions de tierce ou de quinte (état fondamental des accords, en faisant ressortir les notes manquantes).
{{boîte déroulante|Résultat|contenu=
[[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords simplifiés.]]
}}
[[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords simplifiés.]]
Vous pouvez écouter cette partie jouée par un quintuor de cuivres (trompette, bugle, cor, trombone, tuba), donc avec des accords de cinq notes :
: {{lien web
|url=https://www.youtube.com/watch?v=pWfe60nbvjA
|titre = Largo from The New World Symphony by Dvorak
|site=YouTube (The Chamberlain Brass)
|consulté le=2020-12-11
}} : The American Academy of Arts & Letters in New York City (2017).
Nous allons maintenant chiffrer les accords.
Pour établir la basse chiffrée, il nous faut déterminer le parcours harmonique. Pour le premier accord, les tonalités les plus simples avec un ''sol'' dièse sont ''la'' majeur et ''fa'' dièse mineur ; comme le ''mi'' est bécarre, nous retenons ''la'' majeur, il s'agit donc d'un accord de quinte sur la dominante (les accords de dominante étant très utilisés, cela nous conforte dans notre choix). Puis nous avons un ''si'' bémol, nous pouvons être en ''fa'' majeur ou en ''ré'' mineur ; nous retenons ''fa'' majeur, c'est donc le renversement d'un accord sur le degré {{Times New Roman|II}}.
Dans la deuxième mesure, nous revenons en ''la'' majeur, puis, avec un ''la'' et un ''ré'' bémols, nous sommes en ''la'' bémol majeur ; nous avons donc un accord de neuvième incomplet sur la sensible, ou un accord de onzième incomplet sur la dominante.
Dans la troisième mesure, nous passons en ''ré'' majeur, avec un accord de dominante. Puis, nous arrivons dans la tonalité principale, avec le renversement d'un accord de dominante sans tierce suivi d'un accord de tonique. Nous avons donc une cadence parfaite, conclusion logique d'une phrase.
La progression des accords est donc :
{| class="wikitable"
! scope="row" | Tonalité
| ''la'' M - ''fa'' M || ''la'' M - ''la''♭ M || ''ré'' M - ''ré''♭ M || ''ré''♭ M
|-
! scope="row" | Accords
| {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|II}}<sup>6</sup><sub>4</sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|“V”}}<sup>9</sup><sub><s>5</s></sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|V}}<sup>+4</sup> || {{Times New Roman|I}}<sup>5</sup>
|}
Dans le chiffrage jazz, nous avons donc :
* une triade de ''mi'' majeur, E ;
* une triade de ''sol'' majeur avec un ''ré'' en basse : G/D ;
* à nouveau un E ;
* un accord de ''sol'' neuvième diminué incomplet, avec un ''ré'' bémol en basse : G dim<sup>9</sup>/D♭ ;
* un accord de ''la'' majeur, A ;
* un accord de ''la'' bémol septième avec une ''sol'' bémol à la basse : A♭<sup>7</sup>/G♭ ;
* la partie se conclue par un accord parfait de ''ré''♭ majeur, D♭.
Soit une progression E - G/D | E - G dim<sup>9</sup>/D♭ | A - A♭<sup>7</sup>/G♭ | D♭.
[[Fichier:Largo nouveau monde 5 1res mesures accords chiffres.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde en accords simplifiés.]]
{{clear}}
==== Thème de Smoke on the Water ====
Le morceau ''Smoke on the Water'' du groupe Deep Purple (album ''Machine Head'', 1972) possède un célèbre thème, un riff ''({{lang|en|rythmic figure}})'', joué à la guitare sous forme d'accords de puissance ''({{lang|en|power chords}})'', c'est-à-dire des accords sans tierce. Le morceau est en tonalité de ''sol'' mineur naturel (donc avec un ''fa''♮) avec ajout de la note bleue (''{{lang|en|blue note}}'', quinte diminuée, ''ré''♭), et les accords composant le thème sont G<sup>5</sup>, B♭<sup>5</sup>, C<sup>5</sup> et D♭<sup>5</sup>, ce dernier accord étant l'accord sur la note bleue et pouvant être considéré comme une appoggiature (indiqué entre parenthèse ci-après). On a donc ''a priori'', sur les deux premières mesures, une progression {{Times New Roman|I-III-IV}} puis {{Times New Roman|I-III-(♭V)-IV}}. Durant la majeure partie du thème, la guitare basse tient la note ''sol'' en pédale.
{{note|En jazz, la qualité « <sup>5</sup> » indique que l'on n'a que la quinte (et donc pas la tierce), contrairement à la notation de basse chiffrée.}}
: {{lien web
| url = https://www.dailymotion.com/video/x5ili04
| titre = Deep Purple — Smoke on the Water (Live at Montreux 2006)
| auteur = Deep Purple
| site = Dailymotion
| date = 2016 | consulté le = 2020-12-31
}}
Cependant, cette progression forme une mélodie, on peut donc plus la voir comme un contrepoint, la superposition de deux voies ayant un mouvement conjoint, joué par un seul instrument, la guitare, la voie 2 étant jouée une quarte juste en dessous de la voie 1 (la quarte juste descendante étant le renversement de la quinte juste ascendante) :
* voie 1 (aigu) : | ''sol'' - ''si''♭ - ''do'' | ''sol'' - ''si''♭ - (''ré''♭) - ''do'' | ;
* voie 2 (grave) : | ''ré'' - ''fa'' - ''sol'' | ''ré'' - ''fa'' - (''la''♭) - ''sol'' |.
En se basant sur la basse (''sol'' en pédale), nous pouvons considérer que ces deux mesures sont accompagnées d'un accord de Gm<sup>7</sup> (''sol''-''si''♭-''ré''-''fa''), chaque accord de la mélodie comprenant à chaque fois au moins une note de cet accord à l'exception de l'appogiature.
{| class="wikitable"
|+ Mise en évidence des notes de l'accord Gm<sup>7</sup>
|-
! scope="row" | Accords
| G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup>
|-
! scope="row" | Voie 1
| '''''sol''''' || '''''si''♭''' || ''do''
|-
! scope="row" | Voie 2
| '''''ré''''' || '''''fa''''' || '''''sol'''''
|-
! scope="row" | Basse
| '''''sol''''' || '''''sol''''' || '''''sol'''''
|}
Sur les deux mesures suivantes, la basse varie et suit les accords de la guitare avec un retard sur le dernier accord :
{| class="wikitable"
|+ Voies sur les mesure 3-4 du thème
|-
! scope="row" | Accords
| G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> || B♭<sup>5</sup> || G<sup>5</sup>
|-
! scope="row" | Voie 1
| ''sol'' || ''si''♭ || ''do'' || ''si''♭ || ''sol''
|-
! scope="row" | Voie 2
| ''ré'' || ''fa'' || ''sol'' || ''fa'' || ''ré''
|-
! scope="row" | Basse
| ''sol'' || ''sol'' || ''do'' || ''si''♭ || ''si''♭-''sol''
|}
Le couplet de cette chanson est aussi organisé sur une progression de quatre mesures, la guitare faisant des arpèges sur les accords G<sup>5</sup> (''sol''-''ré''-''sol'') et F<sup>5</sup> (''fa''-''do''-''fa'') :
: | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-F<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> |
soit une progression {{Times New Roman|<nowiki>| I-I | I-I | I-VII | I-I |</nowiki>}}. Nous pouvons aussi harmoniser le riff du thème sur cette progression, avec un accord F (''fa''-''la''-''do'') ; nous pouvons aussi nous rappeler que l'accord sur le degré {{Times New Roman|VII}} est plus volontiers considéré comme un accord de septième de dominante {{Times New Roman|V<sup>7</sup>}}, soit ici un accord Dm<sup>7</sup> (''ré''-''fa''-''la''-''do''). On peut donc considérer la progression harmonique sur le thème :
: | Gm-Gm | Gm-Gm | Gm-F ou Dm<sup>7</sup> | Gm-Gm |.
Cette analyse permet de proposer une harmonisation enrichie du morceau, tout en se rappelant qu'une des forces du morceau initial est justement la simplicité de sa structure, qui fait ressortir la virtuosité des musiciens. Nous pouvons ainsi comparer la version album à la version concert avec orchestre ou à la version latino de Pat Boone. À l'inverse, le groupe Psychostrip, dans une version grunge, a remplacé les accords par une ligne mélodique :
* le thème ne contient plus qu'une seule voie (la guitare ne joue pas des accords de puissance) ;
* dans les mesures 9 et 10, la deuxième guitare joue en contrepoint de type mouvement inverse, qui est en fait la voie 2 jouée en miroir ;
* l'arpège sur le couplet est remplacé par une ligne mélodique en ostinato sur une gamme blues.
{| class="wikitable"
|+ Contrepoint sur les mesures 9 et 10
|-
! scope="row" | Guitare 1
| ''sol'' ↗ ''si''♭ ↗ ''do''
|-
! scope="row" | Guitare 2
| ''sol'' ↘ ''fa'' ↘ ''ré''
|}
* {{lien web
| url = https://www.dailymotion.com/video/x5ik234
| titre = Deep Purple — Smoke on the Water (In Concert with the London Symphony Orchestra, 1999)
| auteur = Deep Purple
| site = Dailymotion
| date = 2016 | consulté le = 2020-12-31
}}
* {{lien web
| url = https://www.youtube.com/watch?v=MtUuNzVROIg
| titre = Pat Boone — Smoke on the Water (In a Metal Mood, No More Mr. Nice Guy, 1997)
| auteur = Orrore a 33 Giri
| site = YouTube
| date = 2019-06-24 | consulté le = 2020-12-31
}}
* {{lien web
| url = https://www.youtube.com/watch?v=n7zLlZ8B0Bk
| titre = Smoke on the Water (Heroes, 1993)
| auteur = Psychostrip
| site = YouTube
| date = 2018-06-20 | consulté le = 2020-12-31
}}
== Accords et improvisation ==
Nous avons vu précédemment (chapitre ''[[../Gammes et intervalles#Modes et improvisation|Gammes et intervalles > Modes et improvisation]]'') que le choix d'un mode adapté permet d'improviser sur un accord. L'harmonisation des gammes permet, en inversant le processus, d'étendre notre palette : il suffit de repérer l'accord sur une harmonisaiton de gamme, et d'utiliser cette gamme-là, dans le mode correspondant du degré de l'accord (voir ci-dessus ''[[#Harmonisation par des accords de septième|Harmonisation par des accords de septième]]'').
Par exemple, nous avons vu que l'accord sur le septième degré d'une gamme majeure était un accord demi-diminué ; nous savons donc que sur un accord demi-diminué, nous pouvons improviser sur le mode correspondant au septième degré, soit le mode de ''si'' (locrien).
Un accord de septième de dominante étant commun aux deux tonalités homonymes (par exemple ''fa'' majeur et ''fa'' mineur pour un ''do''<sup>7</sup><sub>+</sub> / C<sup>7</sup>), nous pouvons utiliser le mode de ''sol'' de la gamme majeure (mixolydien) ou de la gamme mineure mineure (mode phrygien dominant, ou phrygien espagnol) pour improviser. Mais l'accord de septième de dominante est aussi l'accord au début d'une grille blues ; on peut donc improviser avec une gamme blues, même si la tierce est majeure dans l'accord et mineure dans la gamme.
[[Fichier:Mode improvisation accords do complet.svg]]
== Autres accords courants ==
[[fichier:Cluster cdefg.png|vignette|Agrégat ''do - ré - mi - fa - sol''.]]
Nous avons vu précédemment l'harmonisation des tonalités majeures et mineures harmoniques par des triades et des accords de septième ; certains accords étant rarement utilisés (l'accord sur le degré {{Times New Roman|III}} et, pour les tonalités mineures harmoniques, l'accord sur la tonique), certains accords étant utilisés comme des accords sur un autre degré (les accords sur la sensible étant considérés comme des accords de dominante sans fondamentale).
Dans l'absolu, on peut utiliser n'importe quelle combinaison de notes, jusqu'aux agrégats, ou ''{{lang|en|clusters}}'' (mot anglais signifiant « amas », « grappe ») : un ensemble de notes contigües, séparées par des intervalles de seconde. Dans la pratique, on reste souvent sur des accords composés de superpositions de tierces, sauf dans le cas de transitions (voir la section ''[[#Notes étrangères|Notes étrangère]]'').
=== En musique classique ===
On utilise parfois des accords dont les notes ne sont pas dans la tonalité (hors modulation). Il peut s'agir d'accords de passage, de notes étrangères, par exemple utilisant un chromatisme (mouvement conjoint par demi-tons).
Outre les accords de passage, les autres accords que l'on rencontre couramment en musique classique sont les accords de neuvième, et les accords de onzième et treizième sur tonique. Ces accords sont simplement obtenus en continuant à empiler les tierces. Il n'y a pas d'accord d'ordre supérieur car la quinzième est deux octaves au-dessus de la fondamentale.
Comme pour les accords de septième, on distingue les accords de neuvième de dominante et les accords de neuvième d'espèce. Dans le cas de la neuvième de dominante, il y a une différence entre les tonalités majeures et mineures : l'intervalle de neuvième est respectivement majeur et mineur. Les chiffrages des renversements peuvent donc différer. Comme pour les accords de septième de dominante, on considère que les accords de septième sur le degré {{Times New Roman|VI}} sont en fait des accords de neuvième de dominante sans fondamentale.
Les accords de neuvième d'espèce sont en général préparés et résolus. Préparés : la neuvième étant une note dissonante (c'est à une octave près la seconde de la fondamentale), l'accord qui précède doit contenir cette note, mais dans un accord consonant ; la neuvième est donc commune avec l'accord précédent. Résolus : la dissonance est résolue en abaissant la neuvième par un mouvement conjoint. Par exemple, en tonalité de ''do'' majeur, si l'on veut utiliser un accord de neuvième d'espèce sur la tonique ''(do - mi - sol - si - ré)'', on peut utiliser avant un accord de dominante ''(sol - si - ré)'' en préparation puis un accord parfait sur le degré {{Times New Roman|IV}} ''(fa - la - do)'' en résolution ; nous avons donc sur la voie la plus aigüe la succession ''ré'' (consonant) - ''ré'' (dissonant) - ''do'' (consonant).
On rencontre également parfois des accords de onzième et de treizième. On omet en général la tierce, car elle est dissonante avec la onzième. L'accord le plus fréquemment rencontré est l'accord sur la tonique : on considère alors que c'est un accord sur la dominante que l'on a enrichi « par le bas », en ajoutant une quinte inférieure. par exemple, dans la tonalité de ''do'' majeur, l'accord ''do - sol - si - ré - fa'' est considéré comme un accord de septième de dominante sur tonique, le degré étant noté « {{Times New Roman|V}}/{{Times New Roman|I}} ». De même pour l'accord ''do - sol - si - ré - fa - la'' qui est considéré comme un accord de neuvième de dominante sur tonique.
=== En jazz ===
En jazz, on utilise fréquemment l'accord de sixte à la place de l'accord de septième majeure sur la tonique. Par exemple, en ''do'' majeur, on utilise l'accord C<sup>6</sup> ''(do - mi - sol - la)'' à la place de C<sup>Δ</sup> ''(do - mi - sol - si)''. On peut noter que C<sup>6</sup> est un renversement de Am<sup>7</sup> et pourrait donc se noter Am<sup>7</sup>/C ; cependant, le fait de le noter C<sup>6</sup> indique que l'on a bien un accord sur la tonique qui s'inscrit dans la tonalité de ''do'' majeur (et non, par exemple, de ''la'' mineur naturelle) — par rapport à l'harmonie fonctionnelle, on remarquera que Am<sup>7</sup> a une fonction tonique, l'utilisation d'un renversement de Am<sup>7</sup> à la place d'un accord de C<sup>Δ</sup> est donc logique.
Les accords de neuvième, onzième et treizième sont utilisés comme accords de septième enrichis. Le chiffrage suit les règles habituelles : on ajoute un « 9 », un « 11 » ou un « 13 » au chiffrage de l'accord de septième.
On utilise également des accords dits « suspendus » : ce sont des accords de transition qui sont obtenus en prenant une triade majeure ou mineure et en remplaçant la tierce par la quarte juste (cas le plus fréquent) ou la seconde majeure. Plus particulièrement, lorsque l'on parle simplement « d'accord suspendu » sans plus de précision, cela désigne l'accord de neuvième avec une quarte suspendue, noté « 9sus4 » ou simplement « sus ».
== L'harmonie tonale ==
L'harmonie tonale est un ensemble de règle assez strictes qui s'appliquent dans la musique savante européenne, de la période baroque à la période classique classique ({{pc|xiv}}<sup>e</sup>-{{pc|xviii}}<sup>e</sup> siècle). Certaines règles sont encore largement appliquées dans divers styles musicaux actuels, y compris populaire (rock, rap…), d'autres sont au contraire ignorées (par exemple, un enchaînement de plusieurs accords de même qualité forme un mouvement parallèle, ce qui est proscrit en harmonie tonale). De nos jours, on peut voir ces règles comme des règles « de bon goût », et leur application stricte comme une manière de composer « à la manière de ».
Précédemment, nous avons vu la progression des accords. Ci-après, nous abordons aussi la manière dont les notes de l'accord sont réparties entre plusieurs voix, et comment on construit chaque voix.
=== Concepts fondamentaux ===
; Consonance
: Les intervalles sont considérés comme « plus ou moins consonants » :
:* consonance parfaite : unisson, quinte et octave ;
:* consonance mixte (parfaite dans certains contextes, imparfaite dans d'autres) : quarte ;
:* consonance imparfaite : tierce et sixte ;
:* dissonance : seconde et septième.
; Degrés
: Certains degrés sont considérés comme « forts », « meilleurs », ce sont les « notes tonales » : {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (sous-dominante) et {{Times New Roman|V}} (dominante).
[[Fichier:Mouvements harmoniques.svg|vignette|upright=0.75|Mouvements harmoniques.]]
; Mouvements
: Le mouvement décrit la manière dont les voix évoluent les unes par rapport aux autres :
:# Mouvement parallèle : les voix sont séparées par un intervalle constant.
:# Mouvement oblique : une voix reste constante, c'est le bourdon ; l'autre monte ou descend.
:# Mouvement contraire : une voix descend, l'autre monte.
:# Échange de voix : les voix échangent de note ; les mélodies se croisent mais on a toujours le même intervalle harmonique.
{{clear}}
=== Premières règles ===
; Règle du plus court chemin
: Quand on passe d'un accord à l'autre, la répartition des notes se fait de sorte que chaque voix fait le plus petit mouvement possible. Notamment : si les deux accords ont des notes en commun, alors les voix concernées gardent la même note.
: Les deux voix les plus importantes sont la voix aigüe — soprano — et la voix la plus grave — basse. Ces deux voix sont relativement libres : la voix de soprano a la mélodie, la voix de basse fonde l'harmonie. La règle du plus court chemin s'applique surtout aux voix intermédiaires ; si l'on a des mouvements conjoints, ou du moins de petits intervalles — c'est le sens de la règle du plus court chemin —, alors les voix sont plus faciles à interpréter. Cette règle évite également que les voix n'empiètent l'une sur l'autre (voir la règle « éviter le croisement des voix »).
; Éviter les consonances parfaites consécutives
:* Lorsque deux voix sont à l'unisson ou à l'octave, elles ne doivent pas garder le même intervalle, l'effet serait trop plat.
:* Lorsque deux voix sont à la quarte ou à la quinte, elles ne doivent pas garder le même intervalle, car l'effet est trop dur.
: Pour éviter cela, lorsque l'on part d'un intervalle juste, on a intérêt à pratiquer un mouvement contraire aux voix qui ne gardent pas la même note, ou au moins un mouvement direct : les voix vont dans le même sens, mais l'intervalle change.
: Notez que même avec le mouvement contraire, on peut avoir des consonances parfaites consécutives, par exemple si une voix fait ''do'' aigu ↗ ''sol'' aigu et l'autre ''sol'' médium ↘ ''do'' grave.
: L'interdiction des consonances parfaites consécutives n'a pas été toujours appliquée, le mouvement parallèle strict a d'ailleurs été le premier procédé utilisé dans la musique religieuse au {{pc|x}}<sup>e</sup> siècle. On peut par exemple utiliser des quintes parallèles pour donner un style médiéval au morceau. On peut également utiliser des octaves parallèles sur plusieurs notes afin de créer un effet de renforcement de la mélodie.
: Par ailleurs, les consonances parfaites consécutives sont acceptées lorsqu'il s'agit d'une cadence (transition entre deux parties ou bien conclusion du morceau).
; Éviter le croisement des voix
: Les voix sont organisées de la plus grave à la plus aigüe. Deux voix n'étant pas à l'unisson, celle qui est plus aigüe ne doit pas devenir la plus grave et ''vice versa''.
; Soigner la partie soprano
: Comme c'est celle qu'on entend le mieux, c'est en général celle qui porte la mélodie principale. On lui applique des règles spécifiques :
:# Si elle chante la sensible dans un accord de dominante ({{Times New Roman|V}}), alors elle doit monter à la tonique, c'est-à-dire que la note suivante sera la tonique située un demi-ton au dessus.
:# Si l'on arrive à une quinte ou une octave entre les parties basse et soprano par un mouvement direct, alors sur la partie soprano, le mouvement doit être conjoint. On doit donc arriver à cette situation par des notes voisines au soprano.
; Préférer certains accords
: Les deux degrés les plus importants sont la tonique ({{Times New Roman|I}}) et la dominante ({{Times New Roman|V}}), les accords correspondants ont donc une importance particulière.
: À l'inverse, l'accord de sensible ({{Times New Roman|VII}}) n'est pas considéré comme ayant une fonction harmonique forte. On le considère comme un accord de dominante affaibli. En tonalité mineure, on évite également l'accord de médiante ({{Times New Roman|III}}).
: Donc on utilise en priorité les accords de :
:# {{Times New Roman|I}} et {{Times New Roman|V}}.
:# Puis {{Times New Roman|II}}, {{Times New Roman|IV}}, {{Times New Roman|VI}} ; et {{Times New Roman|III}} en mode majeur.
:# On évite {{Times New Roman|VII}} ; et {{Times New Roman|III}} en mode mineur.
; Préférer certains enchaînements
: Les enchaînements d'accord peuvent être classés par ordre de préférence. Par ordre de préférence décroissante (du « meilleur » au « moins bon ») :
:# Meilleurs enchaînements : quarte ascendante ou descendante. Notons que la quarte est le renversement de la quinte, on a donc des enchaînements stables et naturels, mais avec un intervalle plus court qu'un enchaînement de quintes.
:# Bons enchaînements : tierce ascendante ou descendante. Les accords consécutifs ont deux notes en commun.
:# Enchaînements médiocres : seconde ascendante ou descendante. Les accords sont voisins, mais ils n'ont aucune note en commun. On les utilise de préférence en mouvement ascendant, et on utilise surtout les enchaînements {{Times New Roman|IV}}-{{Times New Roman|V}}, {{Times New Roman|V}}-{{Times New Roman|VI}} et éventuellement {{Times New Roman|I}}-{{Times New Roman|II}}.
:# Les autres enchaînements sont à éviter.
: On peut atténuer l'effet d'un enchaînement médiocre en plaçant le second accord sur un temps faible ou bien en passant par un accord intermédiaire.
[[Fichier:Progression Vplus4 I6.svg|thumb|Résolution d'un accord de triton (quarte sensible) vers l'accord de sixte de la tonique.]]
; La septième descend par mouvement conjoint
: Dans un accord de septième de dominante, la septième — qui est donc le degré {{Times New Roman|IV}} — descend par mouvement conjoint — elle est donc suivie du degré {{Times New Roman|III}}.
: Corolaire : un accord {{Times New Roman|V}}<sup>+4</sup> se résout par un accord {{Times New Roman|I}}<sup>6</sup> : on a bien un enchaînement {{Times New Roman|V}} → {{Times New Roman|I}}, et la 7{{e}} (degré {{Times New Roman|IV}}), qui est la basse de l'accord {{Times New Roman|V}}<sup>+4</sup>, descend d'un degré pour donner la basse de l'accord {{Times New Roman|I}}<sup>6</sup> (degré {{Times New Roman|III}}).
{{clear}}
[[Fichier:Progression I64 V7plus I5.svg|thumb|Accord de sixte et de quarte cadentiel.]]
; Un accord de sixte et quarte est un accord de passage
: Le second renversement d'un accord parfait est soit une appoggiature, soit un accord de passage, soit un accord de broderie.
: S'il s'agit de l'accord de tonique {{Times New Roman|I}}<sup>6</sup><sub>4</sub>, c'est « accord de sixte et quarte de cadence », l'appoggiature de l'accord de dominante de la cadence parfaite.
{{clear}}
Mais il faut appliquer ces règles avec discernement. Par exemple, la voix la plus aigüe est celle qui s'entend le mieux, c'est donc elle qui porte la mélodie principale. Il est important qu'elle reste la plus aigüe. La voix la plus grave porte l'harmonie, elle pose les accords, il est donc également important qu'elle reste la plus grave. Ceci a deux conséquences :
# Ces deux voix extrêmes peuvent avoir des intervalles mélodiques importants et donc déroger à la règle du plus court chemin : la voix aigüe parce que la mélodie prime, la voix de basse parce que la progression d'accords prime.
# Les croisements des voix intermédiaires sont moins critiques.
Par ailleurs, si l'on applique strictement toutes les règles « meilleurs accords, meilleurs enchaînements », on produit un effet conventionnel, stéréotypé. Il est donc important d'utiliser les solutions « moins bonnes », « médiocres » pour apporter de la variété.
Ajoutons que les renversements d'accords permettent d'avoir plus de souplesse : on reste sur le même accord, mais on enrichit la mélodie sur chaque voix.
Le ''Bolero'' de Maurice Ravel (1928) brise un certain nombre de ces règles. Par exemple, de la mesure 39 à la mesure 59, la harpe joue des secondes. De la mesure 149 à la mesure 165, les piccolo jouent à la sixte, dans des mouvement strictement parallèle, ce qui donne d'ailleurs une sonorité étrange. À partir de la mesure 239, de nombreux instruments jouent en mouvement parallèles (piccolos, flûtes, hautbois, cor, clarinettes et violons).
=== Application ===
[[Fichier:Harmonisation possible de frere jacques exercice.svg|vignette|Exercice : harmoniser ''Frère Jacques''.]]
Harmoniser ''Frère Jacques''.
Nous considérons un morceau à quatre voix : basse, ténor, alto et soprano. La soprano chante la mélodie de ''Frère Jacques''. L'exercice consiste à proposer l'écriture des trois autres voix en respectant les règles énoncées ci-dessus. Pour simplifier, nous ajoutons les contraintes suivantes :
* toutes les voix chantent des blanches ;
* nous nous limitons aux accords de quinte (accords de trois sons composés d'une tierce et d'une quinte) sans avoir recours à leurs renversements (accords de sixte, accords de sixte et de quarte).
Les notes à gauche de la portée indiquent la tessiture (ou ambitus), l'amplitude que peut chanter la voix.
{{clear}}
{{boîte déroulante/début|titre=Solution possible}}
[[Fichier:Harmonisation possible de frere jacques solution.svg|vignette|Harmonisation possible de ''Frère Jacques'' (solution de l'exercice).]]
Il n'y a pas qu'une solution possible.
Le premier accord doit contenir un ''do''. Nous sommes manifestement en tonalité de ''do'' majeur, nous proposons de commencer par l'accord parfait de ''do'' majeur, I<sup>5</sup>.
Le deuxième accord doit comporter un ''ré''. Si nous utilisons l'accord de quinte de ''ré'', nous allons créer une quinte parallèle. Nous pourrions utiliser un renversement, mais nous nous imposons de chercher un autre accord. Il peut s'agir de l'accord ''si''<sup>5</sup> ''(si-ré-fa)'' ou de l'accord de ''sol''<sup>5</sup> ''(sol-si-ré)''. La dernière solution permet d'utiliser l'accord de dominante qui est un accord important de la tonalité. La règle du plus court chemin imposerait le ''sol'' grave pour la partie de basse, mais cela est proche de la limite du chanteur, nous préférons passer au ''sol'' aigu, plus facile à chanter. Nous vérifions qu'il n'y a pas de quinte parallèle : l'intervalle ascendant ''do-sol'' (basse-alto) devient ''sol-si'' (3<sup>ce</sup>), l'intervalle descendant ''do-sol'' (soprano-alto) devient ''ré-si'' (3<sup>ce</sup>).
De la même manière, pour le troisième accord, nous ne pouvons pas passer à un accord de ''la''<sup>5</sup> pour éviter une quinte parallèle. Nous avons le choix entre ''do''<sup>5</sup> ''(do-mi-sol)'' et ''mi''<sup>5</sup> ''(mi-sol-si)''. Nous préférons revenir à l'accord de fondamental, solution très stable (l'enchaînement {{Times New Roman|V}}-{{Times New Roman|I}} formant une cadence parfaite).
Pour le quatrième accord, nous pourrions rester sur l'accord parfait de ''do'' mais cela planterait en quelque sorte la fin du morceau puisque l'on resterait sur la cadence parfaite ; or, nous connaissons le morceau et savons qu'il n'est pas fini. Nous choisissons l'accord de ''la''<sup>5</sup> qui est une sixte ascendante ({{Times New Roman|I}}-{{Times New Roman|VI}}).
Nos aurions pu répartir les voix différemment. Par exemple :
* alto : ''sol''-''si''-''sol''-''do'' ;
* ténor : ''mi''-''ré''-''mi''-''mi''.
{{boîte déroulante/fin}}
[[Fichier:Harmonisation possible de frere jacques.midi|vignette|Fichier son correspondant.]]
{{clear}}
== Annexe ==
=== Accords en musique classique ===
Un accord est un ensemble de notes jouées simultanément. Il peut s'agir :
* de notes jouées par plusieurs instruments ;
* de notes jouées par un même instrument : piano, clavecin, orgue, guitare, harpe (la plupart des instruments à clavier et des instruments à corde).
Pour deux notes jouées simultanément, on parle d'intervalle « harmonique » (par opposition à l'intervalle « mélodique » qui concerne les notes jouées successivement).
Les notes répétées à différentes octaves ne changent pas la nature de l'accord.
La musique classique considère en général des empilements de tierces ; un accord de trois notes sera constitué de deux tierces successives, un accord de quatre notes de trois tierces…
Lorsque tous les intervalles sont des intervalles impairs — tierces, quintes, septièmes, neuvièmes, onzièmes, treizièmes… — alors l'accord est dit « à l'état fondamental » (ou encore « primitif » ou « direct »). La note de la plus grave est appelée « fondamentale » de l'accord. Lorsque l'accord comporte un ou des intervalles pairs, l'accord est dit « renversé » ; la note la plus grave est appelée « basse ».
De manière plus générale, l'accord est dit à l'état fondamental lorsque la basse est aussi la fondamentale. On a donc un état idéal de l'accord (état canonique) — un empilement strict de tierces — et l'état réel de l'accord — l'empilement des notes réellement jouées, avec d'éventuels redoublements, omissions et inversions ; et seule la basse indique si l'accord est à l'état fondamental ou renversé.
Le chiffrage dit de « basse continue » ''({{lang|it|basso continuo}})'' désigne la représentation d'un accord sous la forme d'un ou plusieurs chiffres arabes et éventuellement d'un chiffre romain.
==== Accords de trois notes ====
En musique classique, les seuls accords considérés comme parfaitement consonants, c'est-à-dire sonnant agréablement à l'oreille, sont appelés « accords parfaits ». Si l'on prend une tonalité et un mode donné, alors l'accord construit par superposition es degrés I, III et V de cette gamme porte le nom de la gamme qui l'a généré.
[[fichier:Accord do majeur chiffre.svg|vignette|upright=0.5|Accord parfait de ''do'' majeur chiffré.]]
Par exemple :
* « l'accord parfait de ''do'' majeur » est composé des notes ''do'', ''mi'' et ''sol'' ;
* « l'accord parfait de ''la'' mineur » est composé des notes ''la'', ''do'' et ''mi''.
Un accord parfait majeur est donc composé, en partant de la fondamentale, d'une tierce majeure et d'une quinte juste. Un accord parfait mineur est composé d'une tierce mineure et d'une quinte juste.
L'accord parfait à l'état fondamental est appelé « accord de quinte » et est simplement chiffré « 5 » pour indiquer la quinte.
On peut également commencer un accord sur sa deuxième ou sa troisième note, en faisant monter celle(s) qui précède(nt) à l'octave suivante. On parle alors de « renversement d'accord » ou d'accord « renversé ».
[[Fichier:Accord do majeur renversements chiffre.svg|vignette|upright=0.75|Accord parfait de ''do'' majeur et ses renversements, chiffrés.]]
Par exemple,
* le premier renversement de l'accord parfait de ''do'' majeur est :<br /> ''mi'', ''sol'', ''do'' ;
* le second renversement de l'accord parfait de do majeur est :<br /> ''sol'', ''do'', ''mi''.
Les notes conservent leur nom de « fondamentale », « tierce » et « quinte » malgré le changement d'ordre. La note la plus grave est appelée « basse ».
Dans le cas du premier renversement, le deuxième note est la tierce de la basse (la note la plus grave) et la troisième note est la sixte ; le chiffrage en chiffres arabes est donc « 6 » (puisque l'on omet la tierce) et l'accord est appelé « accord de sixte ». Pour le deuxième renversement, les intervalles sont la quarte et la sixte, le chiffrage est donc « 6-4 » et l'accord est appelé « accord de sixte et de quarte ».
Dans tous les cas, on chiffre le degré on considérant la fondamentale, par exemple {{Times New Roman|I}} si l'accord est construit sur la tonique de la gamme.
Les autres accords de trois notes que l'on rencontre sont :
* l'accord de quinte diminuée, constitué d'une tierce mineure et d'une quinte diminuée ; lorsqu'il est construit sur le septième degré d'une gamme, on considère que c'est un accord de septième de dominante sans fondamentale (voir plus bas), le degré est donc indiqué « “{{Times New Roman|V}}” » (cinq entre guillemets) et non « {{Times New Roman|VII}} » ;
* l'accord de quinte augmenté : il est composé d'une tierce majeure et qu'une quinte augmentée.
Dans le tableau ci-dessous,
* « m » désigne un intervalle mineur ;
* « M » un intervalle majeur ou le mode majeur ;
* « J » un intervalle juste ;
* « d » un intervalle diminué ;
* « A » un intervalle augmenté ;
* « mh » le mode mineur harmonique ;
* « ma » le mode mineur ascendant ;
* « md » le mode mineur descendant.
{| class="wikitable"
|+ Accords de trois notes
! scope="col" rowspan="2" | Nom
! scope="col" rowspan="2" | 3<sup>ce</sup>
! scope="col" rowspan="2" | 5<sup>te</sup>
! scope="col" rowspan="2" | État fondamental
! scope="col" rowspan="2" | 1<sup>er</sup> renversement
! scope="col" rowspan="2" | 2<sup>nd</sup> renversement
! scope="col" colspan="4"| Construit sur les degrés
|-
! scope="col" | M
! scope="col" | mh
! scope="col" | ma
! scope="col" | md
|-
| Accord parfait<br /> majeur || M || J
| accord de quinte || accord de sixte || accord de<br />sixte et de quarte
| {{Times New Roman|I, IV, V}} || {{Times New Roman|V, VI}} || {{Times New Roman|IV, V}} || {{Times New Roman|III, VI, VII}}
|-
| Accord parfait<br /> mineur || m || J
| accord de quinte || accord de sixte || accord de<br />sixte et de quarte
| {{Times New Roman|II, III, VI}} || {{Times New Roman|I, IV}} || {{Times New Roman|I, II}} || {{Times New Roman|I, IV, V}}
|-
| Accord de<br />quinte diminuée || m || d
| accord de<br />quinte diminuée || accord de<br />sixte sensible<br />sans fondamentale || accord de triton<br />sans fondamentale
| {{Times New Roman|VII (“V”)}} || {{Times New Roman|II, VII (“V”)}} || {{Times New Roman|VI, VII (“V”)}} || {{Times New Roman|II}}
|-
| Accord de<br />quinte augmentée || M || A
| accord de<br />quinte augmentée || accord de sixte<br />et de tierce sensible || accord de sixte et de quarte<br />sur sensible
| || {{Times New Roman|III}} || {{Times New Roman|III}} ||
|}
==== Accords de quatre notes ====
Les accords de quatre notes sont des accord composés de trois tierces superposées. La dernière note étant le septième degré de la gamme, on parle aussi d'accords de septième.
Ces accords sont dissonants : ils contiennent un intervalle de septième (soit une octave montante suivie d'une seconde descendante). Ils laissent donc une impression de « tension ».
Il existe sept différents types d'accords, ou « espèces ». Citons l'accord de septième de dominante, l'accord de septième mineure et l'accord de septième majeure.
===== L'accord de septième de dominante =====
[[Fichier:Accord 7e dominante do majeur renversements chiffre.svg|vignette|Accord de septième de dominante de ''do'' majeur et ses renversements, chiffrés.]]
L'accord de septième de dominante est l'empilement de trois tierces à partir de la dominante de la gamme, c'est-à-dire du {{Times New Roman|V}}<sup>e</sup> degré. Par exemple, l'accord de septième de dominante de ''do'' majeur est l'accord ''sol''-''si''-''ré''-''fa'', et l'accord de septième de dominante de ''la'' mineur est ''mi''-''sol''♯-''si''-''ré''. L'accord de septième de dominante dont la fondamentale est ''do'' (''do''-''mi''-''sol''-''si''♭) appartient à la gamme de ''fa'' majeur.
Que le mode soit majeur ou mineur, il est composé d'une tierce majeure, d'une quinte juste et d'une septième mineure (c'est un accord parfait majeur auquel on ajoute une septième mineure). C'est de loin l'accord de septième le plus utilisé ; il apparaît au {{pc|xvii}}<sup>e</sup> en musique classique.
Dans son état fondamental, son chiffrage est {{Times New Roman|V 7/+}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}). Le signe plus indique la sensible.
Son premier renversement est appelé « accord de quinte diminuée et sixte » et est noté {{Times New Roman|V 6/<s>5</s>}} (ou {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}).
Son deuxième renversement est appelé « accord de sixte sensible », puisque la sixte de l'accord est la sensible de la gamme, et est noté {{Times New Roman|V +6}} (ou {{Times New Roman|V<sup>+6</sup>}}).
Son troisième renversement est appelé « accord de quarte sensible » et est noté {{Times New Roman|V +4}} (ou {{Times New Roman|V<sup>+4</sup>}}).
[[Fichier:Accord 7e dominante sans fondamentale do majeur renversements chiffre.svg|vignette|Accord de septième de dominante sans fondamentale de ''do'' majeur et ses renversements, chiffrés.]]
On utilise aussi l'accord de septième de dominante sans fondamentale ; c'est alors un accord de trois notes.
Dans son état fondamental, c'est un « accord de quinte diminuée » placé sur le {{Times New Roman|VII}}<sup>e</sup> degré (mais c'est bien un accord construit sur le {{Times New Roman|V}}<sup>e</sup> degré), noté {{Times New Roman|“V” <s>5</s>}} (ou {{Times New Roman|“V”<sup><s>5</s></sup>}}). Notez les guillemets qui indiquent que la fondamentale V est absente.
Dans son premier renversement, c'est un « accord de sixte sensible sans fondamentale » noté {{Times New Roman|“V” +6/3}} (ou {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}}).
Dans son second renversement, c'est un « accord de triton sans fondamentale » (puisque le premier intervalle est une quarte augmentée qui comporte trois tons) noté {{Times New Roman|“V” 6/+4}} (ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}).
Notons qu'un accord de septième de dominante n'a pas toujours la dominante pour fondamentale : tout accord composé d'une tierce majeure, d'une quinte juste et d'une septième mineure est un accord de septième de dominante et est chiffré {{Times New Roman|<sup>7</sup><sub>+</sub>}}, quel que soit le degré sur lequel il est bâti (certaines notes peuvent avoir une altération accidentelle).
===== Les accords de septième d'espèce =====
Les autres accords de septièmes sont dits « d'espèce ».
L'accord de septième mineure est l'accord de septième formé sur la fondamentale d'une gamme mineure ''naturelle''. Par exemple, l'accord de septième mineure de ''la'' est ''la''-''do''-''mi''-''sol''. Il est composé d'une tierce mineure, d'une quinte juste et d'une septième mineure (c'est un accord parfait mineur auquel on ajoute une septième mineure).
L'accord de septième majeure est l'accord de septième formé sur la fondamentale d'une gamme majeure. Par exemple, L'accord de septième majeure de ''do'' est ''do''-''mi''-''sol''-''si''. Il est composé d'une tierce majeure, d'une quinte juste et d'une septième majeure (c'est un accord parfait majeur auquel on ajoute une septième majeure).
==== Utilisation du chiffrage ====
Le chiffrage est utilisé de deux manières.
La première manière, c'est la notation de la basse continue. La basse continue est une technique d'improvisation utilisée dans le baroque pour l'accompagnement d'instruments solistes. Sur la partition, on indique en général la note de basse de l'accord et le chiffrage en chiffres arabes.
La seconde manière, c'est pour l'analyse d'une partition. Le fait de chiffrer les accords permet de mieux en comprendre la structure.
De manière générale, on peut retenir que :
* le chiffrage « 5 » indique un accord parfait, superposition d'une tierce (majeure ou mineure) et d'une quinte juste ;
* le chiffrage « 6 » indique le premier renversement d'un accord parfait ;
* le chiffrage « 6/4 » indique le second renversement d'un accord parfait ;
* chiffrage « 7/+ » indique un accord de septième de dominante ;
* le signe « + » indique en général que la note de l'intervalle est la sensible ;
* un intervalle barré désigne un intervalle diminué.
[[fichier:Accords gamme do majeur la mineur.svg|class=transparent| center | Principaux accords construits sur les gammes de ''do'' majeur et de ''la'' mineur harmonique.]]
=== Notation « jazz » ===
En jazz et de manière générale en musique rock et populaire, la base d'un accord est la triade composée d'une tierce (majeure ou mineure) et d'une quinte juste. Pour désigner un accord, on utilise la note fondamentale, éventuellement désigné par une lettre dans le système anglo-saxon (A pour ''la'' etc.), suivi d'une qualité (comme « m », « + »…).
Les renversements ne sont pas notés de manière particulière, ils sont notés comme les formes fondamentales.
Dans les deux tableaux suivants, la fondamentale est notée X (remplace le C pour un accord de ''do'', le D pour un accord de ''ré''…). La construction des accords est décrite par la suite.
[[Fichier:Arbre accords triades 5d5J5A.svg|vignette|upright=1.5|Formation des triades présentée sous forme d'arbre.]]
{| class="wikitable"
|+ Notation des principales triades
|-
|
! scope="col" | Tierce<br />mineure (3m)
! scope="col" | Tierce<br />majeure (3M)
|-
! scope="row" | Quinte diminuée (5d)
| X<sup>o</sup>, Xm<sup>♭5</sup>, X–<sup>♭5</sup> ||
|-
! scope="row" | Quinte juste (5J)
| Xm, X– || X
|-
! scope="row" | Quinte augmentée (5A)
| || X+, X<sup>♯5</sup>
|}
[[Fichier:Triades do.svg|class=transparent|center|Triades de do.]]
{| class="wikitable"
|+ Notation des principaux accords de septième
|-
| colspan="2" |
! scope="col" | Tierce<br />mineure (3m)
! scope="col" | Tierce<br />majeure (3M)
|-
! scope="row" rowspan="2" | Quinte<br />diminuée (5d)
! scope="row" | Septième diminuée (7d)
| X<sup>o7</sup> ||
|-
! scope="row" | Septième mineure (7m)
| Xm<sup>7(♭5)</sup>, X–<sup>7(♭5)</sup>, X<sup>Ø</sup> ||
|-
! scope="row" rowspan="3" | Quinte<br />juste (5J)
! scope="row" | Sixte majeure (6M)
| Xm<sup>6</sup> || X<sup>6</sup>
|-
! scope="row" | Septième mineure (7m)
| Xm<sup>7</sup>, X–<sup>7</sup> || X<sup>7</sup>
|-
! scope="row" | Septième majeure (7M)
| Xm<sup>maj7</sup>, X–<sup>maj7</sup>, Xm<sup>Δ</sup>, X–<sup>Δ</sup> || X<sup>maj7</sup>, X<sup>Δ</sup>
|-
! scope="row" rowspan="2" | Quinte<br />augmentée (5A)
! scope="row" | Septième mineure (7m)
| || X+<sup>7</sup>
|-
! scope="row" | Septième majeure (7M)
| || X+<sup>maj7</sup>
|}
[[Fichier:Arbre accords septieme.svg|class=transparent|center|Formation des accords de septième présentée sous forme d'arbre.]]
[[Fichier:Accords do septieme.svg|class=transparent|center|Accord de do septième.]]
On notera que l'intervalle de sixte majeure est l'enharmonique de celui de septième diminuée (6M = 7d).
[[File:Principaux accords do.svg|class=transparent|center|Principaux accords de do.]]
==== Triades ====
; Accords fondés sur une tierce majeure
* accord parfait majeur : pas de notation
*: p. ex. « ''do'' » ou « C » pour l'accord parfait de ''do'' majeur (''do'' - ''mi'' - ''sol'')
; Accords fondés sur une tierce mineure
* accord parfait mineur : « m », « min » ou « – »
*: « ''do'' m », « ''do'' – », « Cm », « C– »… pour l'accord parfait de ''do'' mineur (''do'' - ''mi''♭ - ''sol'')
==== Triades modifiées ====
; Accords fondés sur une tierce majeure
* accord augmenté (la quinte est augmentée) : aug, +, ♯5
*: « ''do'' aug », « ''do'' + », « ''do''<sup>♯5</sup> » « Caug », « C+ » ou « C<sup>♯5</sup> » pour l'accord de ''do'' augmenté (''do'' - ''mi'' - ''sol''♯)
: L'accord augmenté est un empilement de tierces majeures. Ainsi, un accord augmenté a deux notes communes avec deux autres accords augmentés : C+ (''do'' - ''mi'' - ''sol''♯) a deux notes communes avec A♭+ (''la''♭ - ''do'' - ''mi'') et avec E+ (''mi'' - ''sol''♯ - ''si''♯) ; et on remarque que ces trois accords sont en fait enharmoniques (avec les enharmonies ''la''♭ = ''sol''♯ et ''si''♯ = ''do''). En effet, l'octave comporte six tons (sous la forme de cinq tons et deux demi-tons), et une tierce majeure comporte deux tons, on arrive donc à l'octave en ajoutant une tierce majeure à la dernière note de l'accord.
; Accords fondés sur une tierce mineure
* accord diminué (la quinte est diminuée) : dim, o, ♭5
*: « ''do'' dim », « ''do''<sup>o</sup> », « ''do''<sup>♭5</sup> », « Cdim », « C<sup>o</sup> » ou « C<sup>♭5</sup> » pour l'accord de ''do'' diminuné (''do'' - ''mi''♭ - ''sol''♭)
: On remarque que la quinte diminuée est l'enharmonique de la quarte augmentée et est l'intervalle appelé « triton » (car composé de trois tons).
; Accords fondés sur une tierce majeure ou mineure
* accord suspendu de seconde : la tierce est remplacée par une seconde majeure : sus2
*: « ''do''<sup>sus2</sup> » ou « C<sup>sus2</sup> » pour l'accord de ''do'' majeur suspendu de seconde (''do''-''ré''-''sol'')
* accord suspendu de quarte : la tierce est remplacée par une quarte juste : sus4
*: « ''do''<sup>sus4</sup> » ou « C<sup>sus4</sup> » pour l'accord de ''do'' majeur suspendu de quarte (''do''-''fa''-''sol'')
==== Triades appauvries ====
; Accords fondés sur une tierce majeure ou mineure
* accord de puissance : la tierce est omise, l'accord n'est constitué que de la fondamentale et de la quinte juste : 5
*: « ''do''<sup>5</sup> », « C<sup>5</sup> » pour l'accord de puissance de ''do'' (''do'' - ''la'')
{{note|Très utilisé dans les musiques rock, hard rock et heavy metal, il est souvent joué renversé (''la'' - ''do'') ou bien avec l'ajout de l'octave (''do'' - ''la'' - ''do'').}}
==== Triades enrichies ====
; Accords fondés sur une tierce majeure
* accord de septième (la 7<sup>e</sup> est mineure) : 7
*: « ''do''<sup>7</sup> », « C<sup>7</sup> » pour l'accord de ''do'' septième, appelé « accord de septième de dominante de ''fa'' majeur » en musique classique (''do'' - ''mi'' - ''sol'' - ''si''♭)
* accord de septième majeure : Δ, 7M ou maj7
*: « ''do'' <sup>Δ</sup> », « ''do'' <sup>maj7</sup> », « C<sup>Δ</sup> », « C<sup>7M</sup> »… pour l'accord de ''do'' septième majeure (''do'' - ''mi'' - ''sol'' - ''si'')
; Accords fondés sur une tierce mineure
* accord de mineur septième (la tierce et la 7<sup>e</sup> sont mineures) : m7, min7 ou –7
*: « ''do'' m<sup>7</sup> », « ''do'' –<sup>7</sup> », « Cm<sup>7</sup> », « C–<sup>7</sup> »… pour l'accord de ''do'' mineur septième, appelé « accord de septième de dominante de ''fa'' mineur » en musique classique (''do'' - ''mi''♭ - ''sol'' - ''si''♭)
* accord mineure septième majeure : m7M, m7maj, mΔ, –7M, –7maj, –Δ
*: « ''do'' m<sup>7M</sup> », « ''do'' m<sup>maj7</sup> », « ''do'' –<sup>Δ</sup> », « Cm<sup>7M</sup> », « Cm<sup>maj7</sup> », « C–<sup>Δ</sup> »… pour l'accord de ''do'' mineur septième majeure (''do'' - ''mi''♭ - ''sol'' - ''si'')
* accord de septième diminué (la quinte et la septième sont diminuée) : dim 7 ou o7
*: « ''do'' dim<sup>7</sup> », « ''do''<sup>o7</sup> », « Cdim<sup>7</sup> » ou « C<sup>o7</sup> » pour l'accord de ''do'' septième diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si''♭)
* accord demi-diminué (seule la quinte est diminuée, la septième est mineure) : Ø ou –7(♭5)
*: « ''do''<sup>Ø</sup> », « ''do''<sup>7(♭5)</sup> », « C<sup>Ø</sup> » ou « C<sup>7♭5</sup> » pour l'accord de ''do'' demi-diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si'')
=== Construction pythagoricienne des accords ===
Nous avons vu au débuts que lorsque l'on joue deux notes en même temps, leurs vibrations se superposent. Certaines superpositions créent un phénomène de battement désagréable, c'est le cas des secondes.
Dans le cas d'une tierce majeure, les fréquences des notes quadruple et quintuple d'une même base : les fréquences s'écrivent 4׃<sub>0</sub> et 5׃<sub>0</sub>. Cette superposition de vibrations est agréable à l'oreille. Nous avons également vu que dans le cas d'une quinte juste, les fréquences sont le double et le triple d'une même base, ou encore le quadruple et sextuple si l'on considère la moitié de cette base.
Ainsi, dans un accord parfait majeur, les fréquences des fondamentales des notes sont dans un rapport 4, 5, 6. De même, dans le cas d'un accord parfait mineur, les proportions sont de 1/6, 1/5 et 1/4.
{{voir|[[../Caractéristiques_et_notation_des_sons_musicaux#Construction_pythagoricienne_et_gamme_de_sept_tons|Caractéristiques et notation des sons musicaux > Construction pythagoricienne et gamme de sept tons]]}}
=== Un peu de physique : interférences ===
Les sons sont des vibrations. Lorsque l'on émet deux sons ou plus simultanément, les vibrations se superposent, on parle en physique « d'interférences ».
Le modèle le plus simple pour décrire une vibration est la [[w:fr:Fonction sinus|fonction sinus]] : la pression de l'air P varie en fonction du temps ''t'' (en secondes, s), et l'on a pour un son « pur » :
: P(''t'') ≈ sin(2π⋅ƒ⋅''t'')
où ƒ est la fréquence (en hertz, Hz) du son.
Si l'on émet deux sons de fréquence respective ƒ<sub>1</sub> et ƒ<sub>2</sub>, alors la pression vaut :
: P(''t'') ≈ sin(2π⋅ƒ<sub>1</sub>⋅''t'') + sin(2π⋅ƒ<sub>2</sub>⋅''t'').
Nous avons ici une [[w:fr:Identité trigonométrique#Transformation_de_sommes_en_produits,_ou_antilinéarisation|identité trigonométrique]] dite « antilinéarisation » :
: <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{f_1 + f_2}{2}t \right ) \cdot \sin \left ( 2\pi \frac{f_1 - f_2}{2}t \right ).</math>
On peut étudier simplement deux situations simples.
[[Fichier:Battements interferentiels.png|vignette|Deux sons de fréquences proches créent des battements : la superposition d'une fréquence et d'une enveloppe.]]
La première, c'est quand les fréquences ƒ<sub>1</sub> et ƒ<sub>2</sub> sont très proches. Alors, la moyenne (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2 est très proche de ƒ<sub>1</sub> et ƒ<sub>2</sub> ; et la demie différence (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2 est très proche de zéro. On a donc une enveloppe de fréquence très faible, (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2, dans laquelle s'inscrit un son de fréquence moyenne, (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2. C'est cette enveloppe de fréquence très faible qui crée les battements, désagréables à l'oreille.
Sur l'image ci-contre, le premier trait rouge montre un instant où les vibrations sont opposées ; elles s'annulent, le son s'éteint. Le second trait rouge montre un instant où les vibrations sont en phase : elle s'ajoutent, le son est au plus fort.
{{clear}}
La seconde, c'est lorsque les deux fréquences sont des multiples entiers d'une même fréquence fondamentale ƒ<sub>0</sub> : ƒ<sub>1</sub> = ''n''<sub>1</sub>⋅ƒ<sub>0</sub> et ƒ<sub>0</sub> = ''n''<sub>0</sub>⋅ƒ<sub>0</sub>. On a alors :
: <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{n_1 + n_2}{2}f_0 \cdot t \right ) \cdot \sin \left ( 2\pi \frac{n_1 - n_2}{2}f_0 \cdot t \right ).</math>
On multiplie donc deux fonctions qui ont des fréquences multiples de ƒ<sub>0</sub>. La différence minimale entre ''n''<sub>1</sub> et ''n''<sub>2</sub> vaut 1 ; on a donc une enveloppe dont la fréquence est au minimum la moitié de ƒ<sub>0</sub>, c'est-à-dire un son une octave en dessous de ƒ<sub>0</sub>. Donc, cette enveloppe ne crée pas d'effet de battement, ou plutôt, le battement est trop rapide pour être perçu comme tel. Dans cette enveloppe, on a une fonction sinus dont la fréquence est également un multiple de ƒ<sub>0</sub> ; l'enveloppe et la fonction qui y est inscrite ont donc de nombreux « points communs », d'où l'effet harmonieux.
=== Le tonnetz ===
[[File:Speculum musicae.png|thumb|right|225px|Euler, ''De harmoniæ veris principiis'', 1774, p. 350.]]
En allemand, le terme ''Tonnetz'' (se prononce « tône-netz ») signifie « réseau tonal ». C'est une représentation graphique des notes qui a été imaginée par [[w:Leonhard Euler|Leonhard Euler]] en 1739.
Cette représentation graphique peut aider à la mémorisation de certains concepts de l'harmonie. Cependant, son application est très limitée : elle ne concerne que l'intonation juste d'une part, et que les accords parfait des tonalités majeures et mineures naturelles d'autre part. La représentation contenant les douze notes de la musique savante occidentale, on peut bien sûr représenter d'autres objets, comme les accords de septième ou les accords diminués, mais la représentation graphique est alors compliquée et perd son intérêt pédagogique.
On part d'une note, par exemple le ''do''. Si on progresse vers la droite, on monte d'une quinte juste, donc ''sol'' ; vers la gauche, on descend d'une quinte juste, donc ''fa''. Si on va vers le bas, on monte d'une tierce majeure, donc ''mi'' ; si on va vers le haut, on descend d'une tierce majeure, donc ''la''♭ ou ''sol''♯
fa — do — sol — ré
| | | |
la — mi — si — fa♯
| | | |
do♯ — sol♯ — ré♯ — si♭
La figure forme donc un filet, un réseau. On voit que ce réseau « boucle » : si on descend depuis le ''do''♯, on monte d'une tierce majeure, on obtient un ''mi''♯ qui est l'enharmonique du ''fa'' qui est en haut de la colonne. Si on va vers la droite à partir du ''ré'', on obtient le ''la'' qui est au début de la ligne suivante.
Si on ajoute des diagonales allant vers la droite et le haut « / », on met en évidence des tierces mineures : ''la'' - ''do'', ''mi'' - ''sol'', ''si'' - ''ré'', ''do''♯ - ''mi''…
fa — do — sol — ré
| / | / | / |
la — mi — si — fa♯
| / | / | / |
do♯ — sol♯ — ré♯ — si♭
Donc les liens représentent :
* | : tierce majeure ;
* — : quinte juste ;
* / : tierce mineure.
[[Fichier:Tonnetz carre accords fr.svg|thumb|Tonnetz avec les accords parfaits. Les notes sont en notation italienne et les accords en notation jazz.]]
On met ainsi en évidence des triangles dont un côté est une quinte juste, un côté une tierce majeure et un côté une tierce mineure ; c'est-à-dire que les notes aux sommets du triangle forment un accord parfait majeur (par exemple ''do'' - ''mi'' - ''sol'') :
<div style="font-family:courier; background-color:#fafafa">
fa — '''do — sol''' — ré<br />
| / '''| /''' | / |<br />
la — '''mi''' — si — fa♯<br />
| / | / | / |<br />
do♯ — sol♯ — ré♯ — si♭
</div>
ou un accord parfait mineur (''la'' - ''do'' - ''mi'').
<div style="font-family:courier; background-color:#fafafa">
fa — '''do''' — sol — ré<br />
| '''/ |''' / | / |<br />
'''la — mi''' — si — fa♯<br />
| / | / | / |<br />
do♯ — sol♯ — ré♯ — si♭
</div>
Un triangle représente donc un accord, et un sommet représente une note. Si on passe d'un triangle à un triangle voisin, alors on passe d'un accord à un autre accord, les deux accords ayant deux notes en commun. Ceci illustre la notion de « plus court chemin » en harmonie : si on passe d'un accord à un autre en gardant un côté commun, alors on a un mouvement conjoint sur une seule des trois voix.
Par rapport à l'harmonie fonctionnelle : les accords sont contigus à leur fonction, par exemple en ''do'' majeur :
* fonction de tonique ({{Times New Roman|I}}) : C, A– et E– sont contigus ;
* fonction de sous-dominante ({{Times New Roman|IV}}) : F et D– sont contigus ;
* fonction de dominante ({{Times New Roman|V}}) : G et B<sup>o</sup> sont contigus.
On notera que les triangles d'un schéma ''tonnetz'' ne représentent que des accords parfaits. Pour représenter un accord de quinte diminuée (''si'' - ''ré'' - ''fa'') ou les accords de septième, en particulier l'accord de septième de dominante, il faut étendre le ''tonnetz'' et l'on obtient des figures différentes. Par ailleurs, il est adapté à ce que l'on appelle « l'intonation juste », puisque tous les intervalles sont idéaux.
[[Fichier:Tonnetz carre accords etendu fr.svg|vignette|Tonnetz étendu.]]
[[Fichier:Tonnetz carre do majeur accords fr.svg|vignette|Tonnetz de la tonalité de ''do'' majeur. La représentation de l'accord de quinte diminuée sur ''si'' (B<sup>o</sup>) est une ligne et non un triangle.]]
[[Fichier:Tonnetz carre do mineur accords fr.svg|vignette|Tonnetz des tonalités de ''do'' mineur naturel (haut) et ''do'' mineur harmonique (bas).]]
Si l'on étend un peu le réseau :
ré♭ — la♭ — mi♭ — si♭ — fa
| / | / | / | / |
fa — do — sol — ré — la
| / | / | / | / |
la — mi — si — fa♯ — do♯
| / | / | / | / |
do♯ — sol♯ — ré♯ — la♯ — mi♯
| / | / | / | / |
mi♯ — do — sol — ré — la
on peut donc trouver des chemins permettant de représenter les accords de septième de dominante (par exemple en ''do'' majeur, G<sup>7</sup>)
fa
/
sol — ré
| /
si
et des accords de quinte diminuée (en ''do'' majeur : B<sup>o</sup>)
fa
/
ré
/
si
Une gamme majeure ou mineure naturelle peut se représenter par un trapèze rectangle : ''do'' majeur
fa — do — sol — ré
| /
la — mi — si
et ''do'' mineur
la♭ — mi♭ — si♭
/ |
fa — do — sol — ré
En revanche, la représentation d'une tonalité nécessite d'étendre le réseau afin de pouvoir faire figurer tous les accords, deux notes sont représentées deux fois. La représentation des tonalités mineures harmoniques prend une forme biscornue, ce qui nuit à l'intérêt pédagogique de la représentation.
[[Fichier:Neo-Riemannian Tonnetz.svg|vignette|upright=2|Tonnetz avec des triangles équilatéraux.]]
On peut réorganiser le schéma en décalant les lignes, afin d'avoir des triangles équilatéraux. Sur la figure ci-contre (en notation anglo-saxonne) :
* si on monte en allant vers la droite « / », on a une tierce mineure ;
* si on descend en allant vers la droite « \ », on a une tierce majeure ;
* les liens horizontaux « — » représentent toujours des quintes justes
* les triangles pointe en haut sont des accords parfaits mineurs ;
* les triangles pointe en bas sont des accords parfaits majeurs.
On a alors les accords de septième de dominante
F
/
G — D
\ /
B
et de quinte diminuée
F
/
D
/
B
les tonalités majeures
F — C — G — D
\ /
A — E — B
et les tonalités mineures naturelles
A♭ — E♭ — B♭
/ \
F — C — G — D
== Notes et références ==
{{références}}
== Voir aussi ==
=== Liens externes ===
{{wikipédia|Consonance (harmonie tonale)}}
{{wikipédia|Disposition de l'accord}}
{{wikisource|Petit Manuel d’harmonie}}
* {{lien web
| url = https://www.apprendrelesolfege.com/chiffrage-d-accords
| titre = Chiffrage d'accords (classique)
| site = Apprendrelesolfege.com
| consulté le = 2020-12-03
}}
* {{lien web
| url = https://www.coursd-harmonie.fr/introduction/introduction2_le_chiffrage_d_accords.php
| titre = Introduction II : Le chiffrage d'accords
| site = Cours d'harmonie.fr
| consulté le = 2021-12-14
}}
* {{lien web
| url=https://www.coursd-harmonie.fr/
| titre = Cours d'harmonie en ligne
| auteur = Jean-Baptiste Voinet
| site=coursd-harmonie.fr
| consulté le = 2021-12-20
}}
* {{lien web
| url=http://e-harmonie.e-monsite.com/
| titre = Cours d'harmonie classique en ligne
| auteur = Olivier Miquel
| site=e-harmonie
| consulté le = 2021-12-24
}}
* {{lien web
| url=https://fr.audiofanzine.com/theorie-musicale/editorial/dossiers/les-gammes-et-les-modes.html
| titre = Les bases de l’harmonie
| site = AudioFanzine
| date = 2013-07-23
| consulté le = 2024-01-12
}}
----
''[[../Mélodie|Mélodie]]'' < [[../|↑]] > ''[[../Représentation musicale|Représentation musicale]]''
[[Catégorie:Formation musicale (livre)|Harmonie]]
0vs9whb4wm5cznddhxg1uu3akqnttqz
745864
745863
2025-07-03T11:25:30Z
Cdang
1202
/* Harmonisation d'une gamme */ synoptique notation jazz
745864
wikitext
text/x-wiki
{{Bases de solfège}}
<span style="font-size:25px;">6. Harmonie</span>
L'harmonie désigne les notes jouées en même temps, soit plusieurs instruments jouant chacun une note, soit un instrument jouant un accord (instrument dit polyphonique).
== Première approche ==
L'exemple le plus simple d'harmonie est sans doute la chanson en canon : c'est un chant polyphonique, c'est-à-dire à plusieurs voix, chaque voix chantant la même chose en décalé. Prenons par exemple ''Vent frais, vent du matin'' (la version originale est ''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609) :
[[Fichier:Vent frais vent du matin.svg|class=transparent|center|Partition de ''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]]
[[Fichier:Vent frais vent du matin.midi|vignette|''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]]
nous voyons que les voix se superposent de manière « harmonieuse ». Les notes de chaque voix se correspondent point par point (avec un retard), c'est donc un type d'harmonie polyphonique appelé « contrepoint ».
Considérons la première note de la mesure 6 pour chaque voix. Nous avons la superposition des notes ''ré''-''fa''-''la'' (du grave vers l'aigu) ; la superposition de notes jouées ou chantées ensembles s'appelle un accord. Cet accord ''ré''-''fa''-''la'' porte le nom « d'accord parfait de ''ré'' mineur » :
* « ''ré'' » car la note fondamentale est un ''ré'' ;
* « parfait » car il est l'association d'une tierce, ''ré''-''fa'', et d'une quinte juste, ''ré''-''la'' ;
* « mineur » car le premier intervalle, ''ré''-''fa'', est une tierce mineure.
Considérons maintenant un chant accompagné au piano. La piano peut jouer plusieurs notes en même temps, il peut jouer des accords.
[[Fichier:Au clair de le lune chant et piano.svg|class=transparent|center|Deux premières mesure d’Au clair de la lune.]]
[[Fichier:Au clair de le lune chant et piano.midi|vignette|Deux premières mesure d’Au clair de la lune.]]
L'accord, les notes à jouer simultanément, sont écrites « en colonne ». Lorsqu'on les énonce, on les lit de bas en haut mais le pianiste les joue en pressant les touches du clavier en même temps, de manière « plaquée ».
Le premier accord est composé des notes ''do''-''mi''-''sol'' ; il est appelé « accord parfait de ''do'' majeur » car la note fondamentale est ''do'', qu'il est l'association d'une tierce et d'une quinte juste et que le premier intervalle, ''do''-''mi'', est une tierce majeure.
== Consonance et dissonance ==
Les notions de consonance et de dissonance sont culturelles et changent selon l'époque. Nous pouvons néanmoins noter que :
* l'accord de seconde, et son renversement la septième, créent des battements, les notes « frottent », c'est un intervalle harmonique dissonant ; mais dans le cas de la septième, comme les notes sont éloignées, le frottement est moins perceptible ;
* les accords de tierce, quarte et quinte sonnent agréablement à l'oreille, ils sont consonants.
Dans la musique savante européenne, au début au du Moyen-Âge, seuls les accords de quarte et de quinte étaient considérés comme consonants, d'où leur qualification de « juste ». La tierce, et son renversement la sixte, étaient perçues comme dissonantes.
L'harmonie joue avec les consonances et les dissonances. Dans un premier temps, les harmonies dissonantes sont utilisées pour créer des tensions qui sont ensuite résolues, on utilise des successions « consonant-dissonant-consonant ». À force d'entendre des intervalles considérés comme dissonants, l'oreille s'habitue et certains finissent par être considérés comme consonants ; c'est ce qui est arrivé à la tierce et à la sixte à la fin du Moyen Âge avec le contrepoint.
Il faut ici aborder la notion d'harmonique des notes.
[[File:Harmoniques de do.svg|thumb|Les six premières harmoniques de ''do''.]]
Lorsque l'on joue une note, on entend d'autres notes plus aigües et plus faibles ; la note jouée est appelée la « fondamentale » et les notes plus aigües et plus faibles sont les « harmoniques ». C'est cette accumulation d'harmoniques qui donne la couleur au son, son timbre, qui fait qu'un piano ne sonne pas comme un violon. Par exemple, si l'on joue un ''do''<sup>1</sup><ref>Pour la notation des octaves, voir ''[[../Représentation_musicale#Désignation_des_octaves|Représentation musicale > Désignation des octaves]]''.</ref> (fondamentale), on entend le ''do''<sup>2</sup> (une octave plus aigu), puis un ''sol''<sup>2</sup>, puis encore un ''do''<sup>3</sup> plus aigu, puis un ''mi''<sup>3</sup>, puis encore un ''sol''<sup>3</sup>, puis un ''si''♭<sup>3</sup>…
Ainsi, puisque lorsque l'on joue un ''do'' on entend aussi un ''sol'' très léger, alors jouer un ''do'' et un ''sol'' simultanément n'est pas choquant. De même pour ''do'' et ''mi''. De là vient la notion de consonance.
Le statut du ''si''♭ est plus ambigu. Il fait partie des harmoniques qui sonnent naturellement, mais il forme une seconde descendante avec le ''do'', intervalle dissonant. Par ailleurs, on remarque que le ''si''♭ ne fait pas partie de la gamme de ''do'' majeur, contrairement au ''sol'' et au ''mi''.
Pour le jeu sur les dissonances, on peut écouter par exemple la ''Toccata'' en ''ré'' mineur, op. 11 de Sergueï Prokofiev (1912).
: {{lien web |url=https://www.youtube.com/watch?v=AVpnr8dI_50 |titre=Yuja Wang Prokofiev Toccata |site=YouTube |date=2019-02-26 |consulté le=2021-12-19}}
== Contrepoint ==
Dans le chant grégorien, la notion d'accord n'existe pas. L'harmonie provient de la superposition de plusieurs mélodies, notamment dans ce que l'on appelle le « contrepoint ».
Le terme provient du latin ''« punctum contra punctum »'', littéralement « point par point », et désigne le fait que les notes de chaque voix se correspondent.
L'exemple le plus connu de contrepoint est le canon, comme par exemple ''Frère Jacques'' : chaque note d'un couplet correspond à une note du couplet précédent.
Certains morceaux sont bâtis sur une écriture « en miroir » : l'ordre des notes est inversé entre les deux voix, ou bien les intervalles sont inversés (« mouvement contraire » : une tierce montante sur une voix correspond à une tierce descendante sur l'autre).
On peut également citer le « mouvement oblique » (une des voix, le bourdon, chante toujours la même note) et le mouvement parallèle (les deux voix chantent le même air mais transposé, l'une est plus aiguë que l'autre).
Nous reproduisons ci-dessous le début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.
[[Fichier:Haendel Sonate en trio re mineur debut canon.svg | vignette | center | upright=2 | Début du second ''Allergo'' de la sonate en trio en ''ré'' mineur de Haendel.]]
[[Fichier:Haendel Sonate en trio re mineur debut.midi | vignette | Début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.]]
Nous avons mis en évidence la construction en canon avec des encadrés de couleur : sur les quatre premières mesures, nous voyons trois thèmes repris alternativement par une voix et par l'autre. Ce type de procédé est très courant dans la musique baroque.
Les procédés du contrepoint s'appliquent également à la danse :
* unisson : les danseurs et danseuses font les mêmes gestes en même temps ;
* répétition : le fait de répéter une série de gestes, une « phrase dansante » ;
* canon : les gestes sont faits avec un décalage régulier d'un danseur ou d'une danseuse à l'autre ;
* cascade : forme de canon dans laquelle le décalage est très petit ;
* contraste : deux danseur·euses, ou deux groupes, ont des gestuelles très différentes ;
* accumulation : la gestuelle se complexifie par l'ajout d'éléments au fur et à mesure ; ou bien le nombre de danseur·euses augmente ;
* dialogue : les gestes de danseur·euses ou de groupes se répondent ;
* contre-point : la gestuelle d'un ou une danseuse se superpose à la gestuelle d'un groupe ;
* lâcher-rattraper : les danseurs et danseuses alternent danse à l'unisson et gestuelles indépendantes.
: {{lien web
| url=https://www.youtube.com/watch?v=wgblAOzedFc
| titre=Les procédés de composition en danse
| auteur= Doisneau Sport TV
| site=YouTube
| date=2020-03-16 | consulté le=2021-01-21
}}
{{...}}
== Les accords en général ==
Initialement, on a des chants polyphoniques, des voix qui chantent chacune une mélodie, les mélodies se mêlant. On remarque que certaines superpositions de notes sonnent de manière plus ou moins agréables, consonantes ou dissonantes. On en vient alors à associer ces notes, c'est-à-dire à considérer dès le départ la superposition de ces notes et non pas la rencontre de ces notes au gré des mélodies. Ces groupes de notes superposées forment les accords. En Europe, cette notion apparaît vers le {{pc|xiv}}<sup>e</sup> siècle avec notamment la ''[[wikipedia:fr:Messe de Notre Dame|Messe de Notre Dame]]'' de Guillaume de Machaut (vers 1360-1365). La notion « d'accord parfait » est consacrée par [[wikipedia:fr:Jean-Philippe Rameau|Jean-Philippe Rameau]] dans son ''Traité de l'harmonie réduite à ses principes naturels'', publié en 1722.
=== Qu'est-ce qu'un accord ? ===
Un accord est un ensemble d'au minimum trois notes jouées en même temps. « Jouées » signifie qu'il faut qu'à un moment donné, elles sonnent en même temps, mais le début ou la fin des notes peut être à des instants différents.
Considérons que l'on joue les notes ''do'', ''mi'' et ''sol'' en même temps. Cet accord s'appelle « accord de ''do'' majeur ». En musique classique, on lui adjoint l'adjectif « parfait » : « accord parfait de ''do'' majeur ».
Nous représentons ci-dessous trois manière de faire l'accord : avec trois instruments jouant chacun une note :
[[Fichier:Do majeur trois portees.svg|class=transparent|center|Accord de ''do'' majeur avec trois instruments différents.]]
Avec un seul instrument jouant simultanément les trois notes :
[[Fichier:Chord C.svg|class=transparent|center|Accord de ''do'' majeur joué par un seul instrument.]]
L'accord tel qu'il est joué habituellement par une guitare d'accompagnement :
[[Fichier:Do majeur guitare.svg|class=transparent|center|Accord de ''do'' majeur à la guitare.]]
Pour ce dernier, nous représentons le diagramme indiquant la position des doigts sur le manche au dessus de la portée et la tablature en dessous. Ici, c'est au total six notes qui sont jouées : ''mi'' grave, ''do'' médium, ''mi'' médium, ''sol'' médium, ''do'' aigu, ''mi'' aigu. Mais il s'agit bien des trois notes ''do'', ''mi'' et ''sol'' jouées à des octaves différentes. Nous remarquons également que la note de basse (la note la plus grave), ''mi'', est différente de la note fondamentale (celle qui donne le nom à l'accord), ''do'' ; l'accord est dit « renversé » (voir plus loin).
=== Comment joue-t-on un accord ? ===
Les notes ne sont pas forcément jouées en même temps ; elles peuvent être « égrainées », jouée successivement, ce que l'on appelle un arpège. La partition ci-dessous montre six manières différentes de jouer un accord de ''la'' mineur à la guitare, plaqué puis arpégé.
[[Fichier:La mineur differentes executions.svg|class=transparent|center|Différentes exécution de l'accord de do majeur à la guitare.]]
[[Fichier:La mineur differentes executions midi.midi|vignette|Différentes exécution de l'accord de la mineur à la guitare.]]
Vous pouvez écouter l'exécution de cette partition avec le lecteur ci-contre.
Seuls les instruments polyphoniques peuvent jouer les accords plaqués : instruments à clavier (clavecin, orgue, piano, accordéon), les instruments à plusieurs cordes pincées (harpe, guitare ; violon, alto, violoncelle et contrebasse joués en pizzicati). Les instruments à corde frottés de la famille du violon peuvent jouer des notes par deux à l'archet mais pas plus du fait de la forme bombée du chevalet ; cependant, un mouvement rapide permet de jouer les quatre cordes de manière très rapprochée. Les instruments à percussion de type xylophone ou le tympanon permettent de jouer jusqu'à quatre notes simultanément en tenant deux baguettes (mailloches, maillets) par main.
Tous les instruments peuvent jouer des arpèges même si, dans le cas des instruments monodiques, les notes ne continuent pas à sonner lorsque l'on passe à la note suivante.
L'arpège peut être joué par l'instrument de basse (basson, violoncelle, contrebasse, guitare basse, pédalier de l'orgue…), notamment dans le cas d'une basse continue ou d'une ''{{lang|en|walking bass}}'' (« basse marchante » : la basse joue des noires, donnant ainsi l'impression qu'elle marche).
En jazz, et spécifiquement au piano, on a recours au ''{{lang|en|voicing}}'' : on choisit la manière dont on organise les notes pour donner une couleur spécifique, ou bien pour créer une mélodie en enchaînant les accords. Il est fréquent de ne pas jouer toutes les notes : si on n'en garde que deux, ce sont la tierce et la septième, car ce sont celles qui caractérisent l'accord (selon que la tierce est mineure ou majeure, que la septième est majeure ou mineure), et la fondamentale est en général jouée par la contrebasse ou guitare basse.
{{clear}}
=== Classes d'accord ===
[[Fichier:Intervalles harmoniques accords classes.svg|vignette|upright=1.5|Intervalles harmoniques dans les accords classés de trois, quatre et cinq notes.]]
Un accord composé d'empilement de tierces est appelé « accord classé ». En musique tonale, c'est-à-dire la musique fondée sur les gammes majeures ou mineures (cas majoritaire en musique classique), on distingue trois classes d'accords :
* les accords de trois notes, ou triades, ou accords de quinte ;
* les accords de quatre notes, ou accords de septième ;
* les accords de cinq notes, ou accords de neuvième.
En empilant des tierces, si l'on part de la note fondamentale, on a donc de intervalles de tierce, quinte, septième et neuvième.
En musique tonale, les accords avec d'autres intervalles (hors renversement, voir ci-après), typiquement seconde, quarte ou sixte, sont considérés comme des transitions entre deux accords classés. Ils sont appelés, selon leur utilisation, « accords à retard » (en anglais : ''{{lang|en|suspended chord}}'', accord suspendu) ou « appoggiature » (note « appuyée », étrangère à l'harmonie). Voir aussi plus loin la notion de note étrangère.
=== Renversements d'accords ===
[[File:Accord do majeur renversements.svg|thumb|Accord parfait de do majeur et ses renversements.]]
[[Fichier:Progression dominante renverse parfait do majeur.svg|vignette|upright=0.6|Progression accord de dominante renversé → accord parfait en ''do'' majeur.]]
Un accord classé est donc un empilement de tierces. Si l'on change l'ordre des notes, on a toujours le même accord mais il est fait avec d'autres intervalles harmoniques. Par exemple, l'accord parfait de ''do'' majeur dans son état fondamental, c'est-à-dire non renversé, s'écrit ''do'' - ''mi'' - ''sol''. Sa note fondamentale, ''do'', est aussi se note de basse.
Si maintenant on prend le ''do'' de l'octave supérieure, l'accord devient ''mi - sol - do'' ; c'est l'empilement d'une tierce ''(mi - sol)'' et d'une quarte ''(sol - do)'', soit la superposition d'une tierce ''(mi - sol)'' et d'une sixième ''(mi - do)''. C'est le premier renversement de l'accord parfait de ''do'' majeur ; la fondamentale est toujours ''do'' mais la basse est ''mi''. Le second renversement est ''sol - do - mi''.
L'utilisation de renversement peut faciliter l'exécution de la progression d'accord. Par exemple, en tonalité ''do'' majeur, si l'on veut passer de l'accord de dominante ''sol - si - ré'' à l'accord parfait ''do - mi - sol'', alors on peut utiliser le second renversement de l'accord de dominante : ''ré - sol - si'' → ''do - mi - sol''. Ainsi, la basse descend juste d'un ton ''(ré → do)'' et sur un piano, la main reste globalement dans la même position.
Le renversement d'un accord permet également de respecter certaines règles de l'harmonie classique, notamment éviter que des voix se suivent strictement (« mouvement parallèle »), ce qui aurait un effet de platitude.
De manière générale, la notion de renversement permet deux choses :
* d'enrichir l'œuvre : pour créer une harmonie donnée (c'est-à-dire des sons sonnant bien ensemble), nous avons plus de souplesse, nous pouvons organiser ces notes comme nous le voulons selon les voix ;
* de simplifier l'analyse : quelle que soit la manière dont sont organisées les notes, cela nous ramène à un même accord.
{{citation bloc|Or il, y a plusieurs manières de jouer un accord, selon que l'on aborde par la première note qui le constitue, ''do mi sol'', la deuxième, ''mi sol do'', ou la troisième note, ''sol do mi''. Ce sont les renversements, [que Rameau] va classer en différentes combinaisons d'une seule matrice. Faisant cela, Rameau divise le nombre d'accords [de septième] par quatre. Il simplifie, il structure […].|{{ouvrage|prénom1=André |nom1=Manoukian |titre=Sur les routes de la musique |éditeur=Harper Collins |année=2021 |passage=54 |isbn=979-1-03391201-9}} }}
{{clear}}
[[File:Plusieurs realisation 1er renversement doM.svg|thumb|Plusieurs réalisation du premier renversement de l'accord de ''do'' majeur.]]
Notez que dans la notion de renversement, seule importe en fait la note de basse. Ainsi, les accords ''mi-sol-do'', ''mi-do-sol'', ''mi-do-mi-sol'', ''mi-sol-mi-do''… sont tous une déclinaison du premier renversement de ''do-mi-sol'' et ils seront abrégés de la même manière (''mi''<sup>6</sup> en musique classique ou C/E en musique populaire et jazz, voir plus bas).
{{clear}}
== Notation des accords de trois notes ==
Les accords de trois notes sont appelés « accords de quinte » en classique, et « triades » en jazz.
[[Fichier:Progression dominante renverse parfait do majeur chiffrage.svg|vignette|upright=0.7|Chiffrage du second renversement d'un accord de ''sol'' majeur et d'un accord de ''do'' majeur : notation en musique populaire et jazz (haut) et notation de basse chiffrée (bas).]]
Les accords sont construits de manière systématique. Nous pouvons donc les représenter de manière simplifiée. Cette notation simplifiée des accords est appelée « chiffrage ».
Reprenons la progression d'accords ci-dessus : « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » dans la tonalité de ''do'' majeur. On utilise en général trois notations différentes :
* en musique populaire, jazz, rock… un accord est désigné par sa note fondamentale ; ici donc, les accords sont notés « ''sol'' - ''do'' » ou, en notation anglo-saxonne, « G - C » ;<br /> comme le premier accord est renversé, on indique la note de basse après une barre, la progression d'accords est donc chiffrée '''« ''sol''/''ré'' - ''do'' »''' ou '''« G/D - C »''' ;<br /> il s'agit ici d'accords composés d'une tierce majeure et d'une quinte juste ; si les accords sont constitués d'intervalles différents, nous ajoutons un symbole après la note : « m » ou « – » si la tierce est mineure, « dim » ou « ° » si la quinte est diminuée ;
* en musique classique, on utilise la notation de « basse chiffrée » (utilisée notamment pour noter la basse continue en musique baroque) : on indique la note de basse sur la portée et on lui adjoint l'intervalle de la fondamentale à la note la plus haute (donc ici respectivement 6 et 5, puisque ''sol''-''si'' est une sixte et ''do''-''sol'' est une quinte), étant sous-entendu que l'on a des empilements de tierce en dessous ; mais dans le cas du premier accord, le premier intervalle n'est pas une tierce, mais une quarte ''(ré''-''sol)'', on note donc '''« ''ré'' <sup>6</sup><sub>4</sub> - ''do'' <sup>5</sup> »'''<ref>quand on ne dispose pas de la notation en supérieur (exposant) et inférieur (indice), on utilise parfois une notation sous forme de fraction : ''sol'' 6/4 et ''do'' 5/.</ref> ;
* lorsque l'on fait l'analyse d'un morceau, on s'attache à identifier la note fondamentale de l'accord (qui est différente de la basse dans le cas d'un renversement) ; on indique alors le degré de la fondamentale : '''« {{Times New Roman|V<sup>6</sup><sub>4</sub> - I<sup>5</sup>}} »'''.
La notation de basse chiffrée permet de construire l'accord à la volée :
* on joue la note indiquée (basse) ;
* s'il n'y a pas de 2 ni de 4, on lui ajoute la tierce ;
* on ajoute les intervalles indiqués par le chiffrage.
La notation de musique jazz oblige à connaître la composition des différents accords, mais une fois que ceux-ci sont acquis, il n'y a pas besoin de reconstruire l'accord.
La notation de basse chiffrée avec les chiffres romains n'est pas utilisée pour jouer, mais uniquement pour analyser ; Sur les partitions avec basse chiffrée, il y a simplement les chiffrages indiqués au-dessus de la partie de basse. Le chiffrage avec le degré en chiffres romains présente l'avantage d'être indépendant de la tonalité et donc de se concentrer sur la fonction de l'accord au sein de la tonalité. Par exemple, ci-dessous, nous pouvons parler de la progression d'accords « {{Times New Roman|V - I}} » de manière générale, cette notation étant valable quelle que soit la tonalité.
[[File:Progression dominante renverse parfait do majeur chiffrage basse continue.svg|thumb|Chiffrage en notation basse chiffrée de la progression d'accords « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » en do majeur.]]
{{note|En notation de base continue avec fondamentale en chiffres romains, la fondamentale est toujours indiquée ''sous'' la portée de la partie de basse. Les intervalles sont indiqués au-dessus de la portée de la partie de basse ; lorsque l'on fait une analyse, on peut ayssi les indiquer à côté du degré en chiffres romains, donc sous la portée de la basse.}}
{{note|En notation rock, le 5 en exposant indique un accord incomplet avec uniquement la fondamentale et la quinte, un accord sans tierce appelé « accord de puissance » ou ''{{lang|en|power chord}}''. Par exemple, C<sup>5</sup> est l'accord ''do-sol''.}}
{{clear}}
[[Fichier:Accords parfait do majeur basse chiffree fondamental et renverse.svg|vignette|upright=2.5|Chiffrage de l'accord parfait de ''do'' majeur en basse chiffrée, à l'état fondamental et ses renversements.]]
Concernant les accords parfaits en notation de basse chiffrée :
* un accord parfait à l'état fondamental est chiffré « <sup>5</sup> » ; on l'appelle « accord de quinte » ;
* le premier renversement est chiffré « <sup>6</sup> » (la tierce est implicite) ; on l'appelle « accord de sixte » ;
* le second renversement est noté « <sup>6</sup><sub>4</sub> » ; on l'appelle « accord de sixte et de quarte » (ou bien « de quarte et de sixte »).
Par exemple, pour l'accord parfait de ''do'' majeur :
* l'état fondamental ''do''-''mi''-''sol'' est noté ''do''<sup>5</sup> ;
* le premier renversement ''mi''-''sol''-''do'' est noté ''mi''<sup>6</sup> ;
* le second renversement ''sol''-''do''-''mi'' est noté ''sol''<sup>6</sup><sub>4</sub>.
Il y a une exception : l'accord construit sur la sensible (7{{e}} degré) contient une quinte diminuée et non une quinte juste. Le chiffrage est donc différent :
* l'état fondamental ''si''-''ré''-''fa'' est noté ''si''<sup><s>5</s></sup> (cinq barré), « accord de quinte diminuée » ;
* le premier renversement ''ré''-''fa''-''si'' est noté ''ré''<sup>+6</sup><sub>3</sub>, « accord de sixte sensible et tierce » ;
* le second renversement ''fa''-''si''-''ré'' est noté ''fa''<sup>6</sup><sub>+4</sub>, « accord de sixte et quarte sensible ».
Par ailleurs, on ne considère pas qu'il est fondé sur la sensible, mais sur la dominante ; on met donc des guillemets autour du degré, « “V” ». Donc selon l'état, le chiffrage est “V”<sup><s>5</s></sup>, “V”<sup>+6</sup><sub>3</sub> ou “V”<sup>6</sup><sub>+4</sub>.
En notation jazz, on ajoute « dim », « <sup>o</sup> » ou bien « <sup>♭5</sup> » au chiffrage, ici : B dim, B<sup>o</sup> ou B<sup>♭5</sup> pour l'état fondamental. Pour les renversements : B dim/D et B dim/F ; ou bien B<sup>o</sup>/D et B<sup>o</sup>/F ; ou bien B<sup>♭5</sup>/D et B<sup>♭5</sup>/F.
{{clear}}
[[Fichier:Accords basse chiffree basse do fondamental et renverses.svg|vignette|upright=2|Basse chiffrée : accords de quinte, de sixte et de sixte et de quarte ayant pour basse ''do''.]]
Et concernant les accords ayant pour basse ''do'' en tonalité de ''do'' majeur :
* l'accord ''do''<sup>5</sup> est un accord à l'état fondamental, c'est donc l'accord ''do''-''mi''-''sol'' (sa fondamentale est ''do'') ;
* l'accord ''do''<sup>6</sup> est le premier renversement d'un accord, c'est donc l'accord ''do''-''mi''-''la'' (sa fondamentale est ''la'') ;
* l'accord ''do''<sup>6</sup><sub>4</sub> est le second renversement d'un accord, c'est donc l'accord ''do''-''fa''-''la'' (sa fondamentale est ''fa'').
{{clear}}
== Notes étrangères ==
La musique européenne s'appuie essentiellement sur des accords parfaits, c'est-à-dire fondés sur une tierce majeure ou mineure, et une quinte juste. Il arrive fréquemment qu'un accord ne soit pas un accord parfait. Les notes qui font partie de l'accord parfait sont appelées « notes naturelles » et la note qui n'en fait pas partie est appelée « note étrangère ».
Il existe plusieurs types de notes étrangères :
* anticipation : la note étrangère est une note naturelle de l'accord suivant ;
* appogiature : note d'ornementation qui se résout par mouvement conjoint, c'est-à-dire qu'elle est suivie par une note située juste au-dessus ou en dessous (seconde ascendante ou descendante) qui est, elle, une note naturelle ;
* broderie : on part d'une note naturelle, on monte ou on descend d'une seconde, puis on revient sur la note naturelle ;
* double broderie : on part d'une note naturelle, on joue la note du dessus puis la note du dessous avant de revenir à la note naturelle ; ou bien on joue la note du dessous puis la note du dessus ;
* échappée : note étrangère n'appartenant à aucune des autres catégories ;
* note de passage : mouvement conjoint allant d'une note naturelle d'un accord à une note naturelle de l'accord suivant ;
* pédale : la note de basse reste la même pendant plusieurs accords successifs ;
* retard : la note étrangère est une note naturelle de l'accord précédent.
Les notes étrangères ne sont pas chiffrées.
[[File:Notes etrangeres accords.svg|center|Différents types de notes étrangères.]]
{{note|Les anglophones distinguent deux types de retard : la ''{{lang|en|suspension}}'' est résolue vers le haut (le mouvement est ascendant), le ''{{lang|en|retardation}}'' est résolu vers le bas (le mouvement est descendant).}}
== Principaux accords ==
Les trois principaux accords sont :
* l'accord parfait majeur : il est construit sur les degrés {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (médiante) et {{Times New Roman|V}} (dominante) d'une gamme majeure ; il est noté {{Times New Roman|I}}<sup>5</sup>, {{Times New Roman|IV}}<sup>5</sup>, {{Times New Roman|V}}<sup>5</sup> ;
* l'accord parfait mineur : il est construit sur les degrés {{Times New Roman|I}} (tonique) et {{Times New Roman|IV}} (sous-tonique) d'une gamme mineure harmonique ; il est également noté {{Times New Roman|I}}<sup>5</sup> et {{Times New Roman|IV}}<sup>5</sup>, les anglo-saxons le notent {{Times New Roman|i}}<sup>5</sup> et {{Times New Roman|iv}}<sup>5</sup> (la minuscule indiquant le caractère mineur) ;
* l'accord de septième de dominante : il est construit sur le degré {{Times New Roman|V}} (dominante) d'une gamme majeure ou mineure harmonique ; il est noté {{Times New Roman|V}}<sup>7</sup><sub>+</sub>.
On peut trouver ces trois accords sur d'autres degrés, et il existe d'autre types d'accords. Nous verrons cela plus loin.
{| class="wikitable"
|+ Constitution des principaux accords — Dénomination classique
|-
! scope="col" | Accord
! scope="col" | 1<sup>er</sup> intervalle
! scope="col" | 2<sup>e</sup> intervalle
! scope="col" | 3<sup>e</sup> intervalle
|-
! scope="row" | Accord parfait majeur
| tierce majeure (3M) || quinte juste (5J) || —
|-
! scope="row" | Accord parfait mineur
| tierce mineure (3m) || quinte juste (5J) || —
|-
! scope="row" | Accord de septième de dominante
| tierce majeure (3M) || quinte juste (5J) || septième mineure (7m)
|}
{| class="wikitable"
|+ Constitution des principaux accords — Dénomination jazz
|-
! scope="col" | Accord
! scope="col" | 1<sup>er</sup> intervalle
! scope="col" | 2<sup>e</sup> intervalle
! scope="col" | 3<sup>e</sup> intervalle
|-
! scope="row" | Triade majeure
| tierce majeure (3M) || quinte juste (5J) || —
|-
! scope="row" | Triade mineure
| tierce mineure (3m) || quinte juste (5J) || —
|-
! scope="row" | Accord de septième
| tierce majeure (3M) || quinte juste (5J) || septième mineure (7m)
|}
{| border="0"
|-
| [[Fichier:Accord do majeur arpege puis plaque.midi | Accord parfait de ''do'' majeur (C).]] || [[Fichier:Accord do mineur arpege puis plaque.midi | Accord parfait de ''do'' mineur (Cm).]] || [[Fichier:Accord do septieme arpege puis plaque.midi | Accord de septième de dominante de ''fa'' majeur (C<sup>7</sup>).]]
|-
| Accord parfait<br /> de ''do'' majeur (C). || Accord parfait<br /> de ''do'' mineur (Cm). || Accord de septième de dominante<br /> de ''fa'' majeur (C<sup>7</sup>).
|}
'''Rappel :'''
* la tierce mineure est composée d'un ton et demi (1 t ½) ;
* la tierce majeur est composée de deux tons (2 t) ;
* la quinte juste a la même altération que la fondamentale, sauf lorsque la fondamentale est ''si'' (la quinte juste est alors ''fa''♯) ;
* la septième mineure est le renversement de la seconde majeure (1 t).
[[File:Renversements accords pft fa maj basse chiffree.svg|thumb|Renversements de l'accord parfait de ''fa'' majeur, et la notation de basse chiffrée.]]
[[File:Renversements accord sept de dom fa maj basse chiffree.svg|thumb|Renversements de l'accord de septième de dominante de ''fa'' majeur, et la notation de basse chiffrée.]]
{| class="wikitable"
|+ Notation des principaux accords en musique classique
|-
! scope="col" | Accord
! scope="col" | État<br /> fondamental
! scope="col" | Premier<br /> renversement
! scope="col" | Deuxième<br /> renversement
! scope="col" | Troisième<br /> renversement
|-
! scope="row" | Accord parfait
| {{Times New Roman|I<sup>5</sup>}}<br/> acc. de quinte || {{Times New Roman|I<sup>6</sup>}}<br :> acc. de sixte || {{Times New Roman|I<sup>6</sup><sub>4</sub>}}<br /> acc. de quarte et de sixte || —
|-
! scope="row" | Accord de septième<br /> de dominante
| {{Times New Roman|V<sup>7</sup><sub>+</sub>}}<br /> acc.de septième de dominante || {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}<br />acc. de sixte et quinte diminuée || {{Times New Roman|V<sup>+6</sup>}}<br />acc. de sixte sensible || {{Times New Roman|V<sup>+4</sup>}}<br />acc. de quarte sensible<br />acc. de triton
|}
{| class="wikitable"
|+ Notation des principaux accords en jazz
|-
! scope="col" | Accord
! scope="col" | Tierce
! scope="col" | Quinte
! scope="col" | Septième
! scope="col" | Chiffrage
|-
! scope="row" | Triade majeure
| 3M || 5J || || X
|-
! scope="row" | Triade mineure
| 3m || 5J || || Xm, X–
|-
! scope="row" | Septième
| 3M || 5J || 7m || X<sup>7</sup>
|}
En jazz, les renversements se notent en mettant la basse après une barre de fraction, par exemple pour la triade de ''do'' majeur :
* état fondamental : C ;
* premier renversement : C/E ;
* second renversement : C/G.
{{clear}}
Dans le cas d'un accord de septième de dominante, le nom de l'accord change selon que l'on est en musique classique ou en jazz : en musique classique, on donne le nom de la tonalité alors qu'en jazz, on donne le nom de la fondamentale. Ainsi, l'accord appelé « septième de dominante de ''do'' majeur » en musique classique, est appelé « ''sol'' sept » (G<sup>7</sup>) en jazz : la dominante (degré {{Times New Roman|V}}, dominante) de la tonalité de ''do'' majeur est la note ''sol''.
Comment appelle-t-on en musique classique l'accord appelé « ''do'' sept » (C<sup>7</sup>) en jazz ? Les tonalités dont le ''do'' est la dominante sont les tonalités de ''fa'' majeur (''si''♭ à la clef) et de ''fa'' mineur harmonique (''si''♭, ''mi''♭, ''la''♭ et ''ré''♭ à la clef et ''mi''♮ accidentel). Il s'agit donc de l'accord de septième de dominante des tonalités de ''fa'' majeur et ''fa'' mineur harmonique.
{| class="wikitable"
|+ Accords fréquents pour quelques la tonalités majeures
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Accord parfait<br />{{Times New Roman|I<sup>5</sup>}}
! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}}
|-
|''Do'' majeur || || C<br />''do-mi-sol'' || G7<br />''sol-si-ré-fa''
|-
|''Sol'' majeur || ''fa''♯ || G<br />''sol-si-ré'' || D7<br />''ré-fa''♯''-la-do''
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || D<br />''ré-fa''♯''-la'' || A7<br />''la-do''♯''-mi-sol''
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A<br />''la-do''♯''-mi'' || E7<br />''mi-sol''♯''-si-ré''
|-
| ''Fa'' majeur || ''si''♭ || F<br />''fa-la-do'' || C7<br />''do-mi-sol-si''♭
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭<br />''si''♭''-ré-fa'' || F7<br />''fa-la-do-mi''♭
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭<br />''mi''♭''-sol-si''♭ || B♭7<br />''si''♭''-ré-fa-la''♭
|}
{| class="wikitable"
|+ Accords fréquents pour quelques la tonalités mineures harmoniques
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Accord parfait<br />{{Times New Roman|i<sup>5</sup>}}
! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}}
|-
|''La'' mineur<br />harmonique || || Am, A–<br />''la-do-mi'' || E7<br />''mi-sol''♯''-si-ré''
|-
|''Mi'' mineur<br />harmonique || ''fa''♯ || Em, E–<br />''mi-sol-si'' || B7<br />''si-ré''♯''-fa''♯''-la''
|-
|''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || Bm, B–<br />''si-ré-fa''♯ || F♯7<br />''fa''♯''la''♯''-do''♯''-mi''
|-
|''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || F♯m, F♯–<br />''fa''♯''-la-do''♯ || C♯7<br />''do''♯''-mi''♯''-sol''♯''-si''
|-
| ''Ré'' mineur<br />harmonique || ''si''♭ || Dm, D–<br />''ré-fa-la'' || A7<br />''la-do''♯''-mi-sol''
|-
| ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || Gm, G–<br />''sol-si''♭''-ré'' || D7<br />''ré-fa''♯''-la-do''
|-
| ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || Cm, C–<br />''do-mi''♭''-sol'' || G7<br />''sol-si''♮''-ré-fa''
|}
{{clear}}
== Accords sur les degrés d'une gamme ==
=== Harmonisation d'une gamme ===
[[Fichier:Accord trois notes gamme do majeur chiffre.svg|vignette|upright=1.2|Accords de trois note sur la gamme de ''do'' majeur, chiffrés.]]
On peut ainsi construire une triade par degré d'une gamme.
Pour une gamme majeure, les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|V<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|II<sup>5</sup>}}, {{Times New Roman|III<sup>5</sup>}}, {{Times New Roman|VI<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|ii<sup>5</sup>}}, {{Times New Roman|iii<sup>5</sup>}}, {{Times New Roman|vi<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}.
Les accords ont tous une quinte juste à l'exception de l'accord {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} qui a une quinte diminuée, raison pour laquelle le « 5 » est barré. C'est un accord dit « de quinte diminuée ». En jazz, l'accord diminué est noté « dim », « ° », « m<sup>♭5</sup> » ou « <sup>–♭5</sup> ».
Nous avons donc trois types d'accords (dans la notation jazz) : X (triade majeure), Xm (triade mineure) et X° (triade diminuée), la lettre X remplaçant le nom de la note fondamentale.
{{clear}}
[[Fichier:Accord trois notes gamme la mineur chiffre.svg|vignette|upright=1.2|Accords de trois notes sur une gamme de ''la'' mineur harmonique, chiffrés.]]
Pour une gamme mineure harmonique, les accords {{Times New Roman|III<sup>+5</sup>}}, {{Times New Roman|V<sup>♯</sup>}} et {{Times New Roman|VI<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|II<sup><s>5</s></sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|i<sup>5</sup>}}, {{Times New Roman|ii<sup><s>5</s></sup>}}, {{Times New Roman|iv<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}.
Les accords {{Times New Roman|ii<sup><s>5</s></sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} ont une quinte diminuée ; ce sont des accords dits « de quinte diminuée ». L'accord {{Times New Roman|III<sup>+5</sup>}} a une quinte augmentée ; le signe « plus » indique que la note de cinquième, le ''sol'' dièse, est la sensible. En jazz, l'accord est noté « aug » ou « <sup>+</sup> ». Les autres accords ont une quinte juste.
Aux trois accords générés par une gamme majeure (X, Xm et X°), nous voyons ici apparaître un quatrième type d'accord : la triade augmentée X<sup>+</sup>.
Nous remarquons que des gammes ont des accords communs. Par exemple, l'accord {{Times New Roman|ii<sup>5</sup>}} de ''do'' majeur est identique à l'accord {{Times New Roman|iv<sup>5</sup>}} de ''la'' mineur (il s'agit de l'accord Dm).
Quel que soit le mode, les accords construits sur la sensible (accord de quinte diminuée) sont rarement utilisés. S'ils le sont, c'est en tant qu'accord de septième de dominante sans fondamentale (voir ci-après). C'est la raison pour laquelle le chiffrage indique le degré {{Times New Roman|V}} entre guillemets, et non pas le degré {{Times New Roman|VII}} (mais pour des raisons de clarté, nous l'indiquons entre parenthèses au début).
En mode mineur, l'accord de quinte augmentée {{Times New Roman|iii<sup>+5</sup>}} est très peu utilisé (voir plus loin ''[[#Progression_d'accords|Progression d'accords]]''). C'est un accord considéré comme dissonant.
On voit que :
* un accord parfait majeur peut appartenir à cinq gammes différentes ;<br /> par exemple l'accord parfait de ''do'' majeur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> degré de la gamme de ''do'' majeur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' mineur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''mi'' mineur ;
* un accord parfait mineur peut appartenir à cinq gammes différentes ;<br />par exemple l'accord parfait de ''la'' mineur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> de la gamme de ''la'' mineur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''mi'' mineur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|III}}<sup>e</sup> degré de ''fa'' majeur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''do'' majeur ;
* un accord de quinte diminuée peut appartenir à trois gammes différentes ;<br />par exemple, l'accord de quinte diminuée de ''si'' est l'accord construit sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' majeur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''la'' mineur et sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' mineur ;
* un accord de quinte augmentée (à l'état fondamental) ne peut appartenir qu'à une seule gamme ;<br /> par exemple, l'accord de quinte augmentée de ''do'' est l'accord construit sur le {{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur.
{| class="wikitable"
|+ Notation jazz des triades
|-
| rowspan="2" colspan="2" |
! scope="col" colspan="2" | Tierce
|-
! scope="col" | 3m
! scope="col" | 3M
|-
! rowspan="3" | Quinte
! scope="row" | 5d
| Xᵒ, X<sub>m</sub><sup>(♭5)</sup> ||
|-
! scope="row" | 5J
| Xm, X– || X
|-
! scope="row" | 5A
| || X+, X<sup>(♯5)</sup>
|}
=== Harmonisation par des accords de septième ===
[[Fichier:Harmonisation gamme do majeur par septiemes chiffre.svg|vignette|upright=2|Harmonisation de la gamme de do majeur par des accords de septième.]]
Les accords de septième contiennent une dissonance et créent ainsi une tension. Ils sont très utilisés en jazz. Nous avons représenté ci-contre l'harmonisation de la gamme de ''do'' majeur.
La constitution des accords est la suivantes :
* tierce majeure (3M)
** quinte juste (5J)
*** septième mineure (7m) : sur le degré V, c'est l'accord de septième de dominante V<sup>7</sup><sub>+</sub>, noté X<sup>7</sup> (X pour G),
*** septième majeure (7M) : sur les degrés I et IV, appelés « accords de septième majeure » et notés aussi X<sup>maj7</sup> ou X<sup>Δ</sup> (X pour C ou F) ;
* tierce mineure (3m)
** quinte juste (5J)
*** septième mineure : sur les degrés ii, iii et vi, appelés « accords mineur septième » et notés Xm<sup>7</sup> ou X–<sup>7</sup> (X pour D, E ou A),
** quinte diminuée (5d)
*** septième mineure (7m) : sur le degré vii, appelé « accord demi-diminué » (puisque seule la quinte est diminuée) et noté X<sup>∅</sup> ou Xm<sup>7(♭5)</sup> ou X–<sup>7(♭5)</sup> (X pour B) ;<br /> en musique classique, on considère que c'est un accord de neuvième de dominante sans fondamentale.
Nous avons donc quatre types d'accords : X<sup>7</sup>, X<sup>maj7</sup>, Xm<sup>7</sup> et X<sup>∅</sup>
En jazz, on ajoute souvent la quarte à l'accord de sous-dominante IV (sur le ''fa'' dans une gamme de ''do'' majeur) ; il s'agit ici d'une quarte augmentée (''fa''-''si'') et l'accord est surnommé « accord lydien » mais cette dénomination est erronée (il s'agit d'une mauvaise interprétation de textes antiques). C'est un accord de onzième sans neuvième (la onzième étant l'octave de la quarte), il est noté X<sup>maj7(♯11)</sup> ou X<sup>Δ(♯11)</sup> (ici, F<sup>maj7(♯11)</sup>, ''fa''-''la''-''do''-''mi''-''si'' ou ''fa''-''la''-''si''-''do''-''mi'').
=== Modulation et emprunt ===
Un morceau peut comporter des changements de tonalité; appelés « modulation ». Il y a parfois un court passage dans une tonalité différente, typiquement sur une ou deux mesures, avant de retourner dans la tonalité d'origine : on parle d'emprunt. Lorsqu'il y a une modulation ou un emprunt, les degrés changent. Un même accord peut donc avoir une fonction dans une partie du morceau et une autre fonction ailleurs. L'utilisation d'accord différents, et en particulier d'accord utilisant des altérations accidentelles, indique clairement une modulation.
Nous avons vu précédemment que les modulations courantes sont :
* les modulations dans les tons voisins ;
* les modulations homonymes ;
* les marches harmoniques.
Une modulation entre une tonalité majeure et mineure change la couleur du passage,
* la modulation la plus « douce » est entre les tonalités relatives (par exemple''do'' majeur et ''la'' mineur) car ces tonalités utilisent quasiment les mêmes notes ;
* la modulation la plus « voyante » est la modulation homonyme (par exemple entre ''do'' majeur et ''do'' mineur).
Une modulation commence souvent sur l'accord de dominante de la nouvelle tonalité.
Pour analyser un œuvre, ou pour improviser sur une partie, il est important de reconnaître les modulations. La description de la successind es tonalités s'appelle le « parcours tonal ».
=== Exercices élémentaires ===
L'apprentissage des accords passe par quelques exercices élémentaires.
'''1. Lire un accord'''
Il s'agit de lecture de notes : des notes composant les accords sont écrites « empilées » sur une portée, il faut les lire en énonçant les notes de bas en haut.
'''2. Reconnaître la « couleur » d'un accord'''
On écoute une triade et il faut dire si c'est une triade majeure ou mineure. Puis, on complexifie l'exercice en ajoutant la septième.
'''3. Chiffrage un accord'''
Trouver le nom d'un accord à partir des notes qui le composent.
'''4. Réalisation d'un accord'''
Trouver les notes qui composent un accord à partir de son nom.
'''5. Dictée d'accords'''
On écoute une succession d'accords et il faut soit écrire les notes sur une portée, soit écrire les noms de accords.
[[File:Exercice constitution accord basse chiffree.svg|thumb|Exercice : constitution d'accord à partir de la basse chiffrée.]]
'''Exercices de basse chiffrée'''
''Réalisation d'un accord''
Sur la figure suivante, écrire les notes des accords correspondant à la basse chiffrée. Déterminer le degré de la fondamentale pour chaque accord en considérant que nous sommes dans la tonalité de ''sol'' majeur.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice constitution accord basse chiffree solution.svg|vignette|Solution.]]
# La note de basse est un ''do''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''mi'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''sol''.<br />Le chiffrage « <sup>5</sup> » indique que c'est un accord dans son état fondamental (l'écart entre deux notes consécutives ne dépasse pas la tierce), la fondamentale est donc la basse, ''do'', qui est le degré IV de la tonalité.
# La note de basse est un ''si''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''ré'', puis nous appliquons le chiffrage 6 et ajoutons la sixte, ''sol''.<br />Le chiffrage « <sup>6</sup> » indique que c'est un accord dans son premier renversement. En le remettant dans son état fondamental, nous obtenons ''sol-si-ré'', la fondamentale est donc la tonique, le degré I.
# La note de basse est un ''la''. Nous ajoutons la tierce (chiffre 3), ''do'', et la sixte (6), ''fa''♯. Nous vérifions que le ''fa''♯ est la sensible (signe +)<br />Nous voyons un « blanc » entre les notes ''do'' et ''fa''♯. En descendant le ''fa''♯ à l'octave inférieure, nous obtenons un empilement de tierces ''fa''♯-''la-do'', le fondamentale est donc ''fa''♯, le degré VII. Nous pouvons le voir comme le deuxième renversement de l'accord de septième de dominante, sans fondamentale.
# La note de basse est un ''fa''♯. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''la'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''do'' ; nous vérifions qu'il s'agit bien d'une quinte diminuée (le 5 est barré). Nous appliquons le chiffre 6 et ajoutons la sixte, ''ré''.<br />Nous voyons que les notes ''do'' et ''ré'' sont conjointes (intervalle de seconde). En descendant le ''ré'' à l'octave inférieure, nous obtenons un empilement de tierces ''ré-fa''♯-''la-do'', le fondamentale est donc ''ré'', le degré V. Nous constatons que l'accord chiffré est le premier renversement de l'accord de septième de dominante.
{{boîte déroulante/fin}}
{{clear}}
''Chiffrage d'accords''
[[Fichier:Exercice chiffrage accord basse chiffree.svg|vignette|Accords à chiffrer.]]
Chiffrer les accords ci-contre.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice chiffrage accord basse chiffree solution.svg|vignette|Solution.]]
# On relève les intervalles en partant de la basse : tierce majeure (3M) et quinte juste (5J). Le chiffrage complet est donc ''fa''<sup>5</sup><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''fa''<sup>5</sup>.<br /> On peut aussi reconnaître que c'est l'accord parfait sur la tonique de la tonalité de ''fa'' majeur dans son état fondamental, le chiffrage d'un accord parfait étant <sup>5</sup>.
# On relève les intervalles en partant de la basse : quarte juste (4J), sixte majeure (6M). Le chiffrage complet est donc ''fa''<sup>6</sup><sub>4</sub>.<br /> On peut aussi reconnaître que c'est le second renversement de l'accord ''mi-sol-si'', sur la tonique de la tonalité de ''mi'' mineur, le chiffrage du second renversement d'un accord parfait étant <sup>6</sup><sub>4</sub>.
# Les intervalles en partant de la basse sont : tierce mineure (3m), quinte diminuée (5d), sixte mineure (6m). Le chiffrage complet est donc ''mi''<sup>6</sup><small><s>5</s></small><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''mi''<sup>6</sup><sub><s>5</s></sub>.<br /> On reconnaît le premier renversement de l'accord ''do-mi-sol-si''♭, accord de septième de dominante de la tonalité de ''fa'' majeur.
# Les intervalles en partant de la basse sont : tierce mineure (3m), quinte juste (5J), septième mineure (7m). Le chiffrage complet est donc ''ré''<sup>7</sup><small>5</small><sub>3</sub> ; c'est typique d'un accord de septième de dominante, son chiffrage est donc ''ré''<sup>7</sup><sub>+</sub>.<br /> On reconnaît l'accord de septième de dominante de la tonalité de ''sol'' mineur dans son état fondamental.
{{boîte déroulante/fin}}
{{clear}}
[[File:Exercice constitution accord notation jazz.svg|thumb|Exercice : constitution d'un accord d'après son chiffrage en notation jazz.]]
'''Exercices de notation jazz'''
''Réalisation d'un accord''
Sur la figure suivante, écrire les notes des accords correspondant aux chiffrages.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice constitution accord notation jazz solution.svg|thumb|Solution.]]
# Il s'agit de la triade majeure de ''do'' dans son état fondamental. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''do-mi-sol''.
# Il s'agit de la triade majeure de ''sol''. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''sol-si-ré''. On renverse l'accord afin que la basse soit le ''si'', l'accord est donc ''si-ré-sol''.
# Il s'agit de l'accord demi-diminué de ''fa''♯. Les intervalles sont la tierce mineure (3m), la quinte diminuée (5d) et la septième mineure (7m). Les notes sont donc ''fa''♯-''la-do-mi''. Nous renversons l'accord afin que la basse soit le ''la'', l'accord est donc ''a-do-mi-fa''♯.
# Il s'agit de l'accord de septième de ''ré''. Les intervalles sont donc la tierce majeure (3M), la quinte juste (5J) et la septième mineure (7m). Les notes sont ''ré-fa''♯''-la-do''. Nous renversons l'accord afin que la basse soit le ''fa''♯, l'accord est donc ''fa''♯''-la-do-ré''.
{{boîte déroulante/fin}}
{{clear}}
''Chiffrage d'accords''
[[File:Exercice chiffrage accord notation jazz.svg|thumb|Accords à chiffrer.]]
Chiffrer les accords ci-contre.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice chiffrage accord notation jazz solution.svg|thumb|Solution.]]
# Les notes sont toutes sur des interlignes consécutifs, c'est donc un empilement de tierces ; l'accord est dans son état fondamental. Les intervalles sont une tierce majeure (''fa-la'' : 3M) et une quinte juste (''fa-do'' : 5J), c'est donc la triade majeure de ''fa''. Le chiffrage est F.
# Il y a un blanc dans l'empilement des notes, c'est donc un accord renversé. En permutant les notes pour n'avoir que des tierces, on trouve l'accord ''mi-sol-si''. Les intervalles sont une tierce mineure (''mi-sol'' : 3m) et une quinte juste (''mi-si'' : 5J), c'est donc la triade mineure de ''mi'' avec un ''si'' à la basse. Le chiffrage est Em/B ou E–/B.
# Il y deux notes conjointes, c'est donc un renversement. L'état fondamental de cet accord est ''do-mi-sol-si''♭. Les intervalles sont une tierce majeure (''do-mi'' : 3M), une quinte juste (''do-sol'' : 5J) et une septième mineure (''do-si''♭ : 7m). C'est donc l'accord de ''do'' septième avec un ''mi'' à la basse, chiffré C<sup>7</sup>/E.
# Les notes sont toutes sur des interlignes consécutifs, l'accord est dans son état fondamental. Les intervalles sont la tierce mineure (''ré-fa'' : 3m), une quinte juste (''ré-la'' : 5J) et une septième mineure (''ré-do'' : 7m). C'est donc l'accord de ''ré'' mineur septième, chiffré Dm<sup>7</sup> ou D–<sup>7</sup>.
{{boîte déroulante/fin}}
{{clear}}
== Harmonie fonctionnelle ==
Le choix des accords et de leur succession — la progression des accords — est un élément important d'un morceau, de sa composition. Le compositeur ou la compositrice a bien sûr une liberté totale, mais pour faire des choix, il faut comprendre les conséquences de ces choix, et donc ici, les effets produits par les accords et leur progression.
Une des manières d'aborder le sujet est l'harmonie fonctionnelle.
=== Les trois fonctions des accords ===
En harmonie tonale, on considère que les accords ont une fonction. Il existe trois fonctions :
* la fonction de tonique, {{Times New Roman|I}} ;
* la fonction de sous-dominante, {{Times New Roman|IV}} ;
* la fonction de dominante, {{Times New Roman|V}}.
L'accord de tonique, {{Times New Roman|I}}, est l'accord « stable » de la tonalité par excellence. Il conclut en général les morceaux, et ouvre souvent les morceaux ; il revient fréquemment au cours du morceau.
L'accord de dominante, {{Times New Roman|V}}, est un accord qui introduit une instabilité, une tension. En particulier, il contient la sensible (degré {{Times New Roman|VI}}), qui est une note « aspirée » vers la tonique. Cette tension, qui peut être renforcée par l'utilisation d'un accord de septième, est fréquemment résolue par un passage vers l'accord de tonique. Nous avons donc deux mouvements typiques : {{Times New Roman|I}} → {{Times New Roman|V}} (création d'une tension, d'une attente) et {{Times New Roman|V}} → {{Times New Roman|I}} (résolution d'une tension). Les accords de tonique et de dominante ont le cinquième degré en commun, cette note sert donc de pivot entre les deux accords.
L'accord de sous-dominante, {{Times New Roman|IV}}, est un accord qui introduit lui aussi une tension, mais moins grande : il ne contient pas la sensible. Notons que s'il est une quarte au-dessus de la tonique, il est aussi une quinte en dessous d'elle ; il est symétrique de l'accord de dominante. Il a donc un rôle similaire à l'accord de dominante, mais atténué. L'accord de sous-dominante aspire soit vers l'accord de dominante, très proche, et l'on a alors une augmentation de la tension ; soit vers l'accord de tonique, un retour vers la stabilité (il a alors un rôle semblable à la dominante). Du fait de ces deux bifurcations possibles — augmentation de la tension ({{Times New Roman|IV}} → {{Times New Roman|V}}) ou retour à la stabilité ({{Times New Roman|IV}} → {{Times New Roman|I}}) —, l'utilisation de l'accord de sous-dominante introduit un certain flottement : si l'on peut facilement prédire l'accord qui suit un accord de dominante, on ne peut pas prédire ce qui suit un accord de sous-dominante.
Notons que la composition ne consiste pas à suivre ces règles de manière stricte, ce qui conduirait à des morceaux stéréotypés et plats. Le plaisir d'écoute joue sur une alternance entre satisfaction d'une attente (respect des règles) et surprise (rompre les règles).
=== Accords remplissant ces fonctions ===
Les accords sur les autres degrés peuvent se ramener à une de ces trois fonctions :
* {{Times New Roman|II}} : fonction de sous-dominante {{Times New Roman|IV}} ;
* {{Times New Roman|III}} (très peu utilisé en mode mineur en raison de sa dissonance) et {{Times New Roman|VI}} : fonction de tonique {{Times New Roman|I}} ;
* {{Times New Roman|VII}} : fonction de dominante {{Times New Roman|V}}.
En effet, les accords étant des empilements de tierces, des accords situés à une tierce l'un de l'autre — {{Times New Roman|I}} ↔ {{Times New Roman|III}}, {{Times New Roman|II}} ↔ {{Times New Roman|IV}}, {{Times New Roman|V}} ↔ {{Times New Roman|VII}}, {{Times New Roman|VI}} ↔ {{Times New Roman|VIII}} ( = {{Times New Roman|I}}) — ont deux notes en commun. On retrouve le fait que l'accord sur le degré {{Times New Roman|VII}} est considéré comme un accord de dominante sans tonique. En mode mineur, l'accord sur le degré {{Times New Roman|III}} est évité, il n'a donc pas de fonction.
{|class="wikitable"
|+ Fonction des accords
|-
! scope="col" | Fondamentale
! scope="col" | Fonction
|-
| {{Times New Roman|I}} || tonique
|-
| {{Times New Roman|II}} || sous-dominante faible
|-
| {{Times New Roman|III}} || tonique faible
|-
| {{Times New Roman|IV}} || sous-dominante
|-
| {{Times New Roman|V}} || dominante
|-
| {{Times New Roman|VI}} || tonique faible
|-
| {{Times New Roman|VII}} || dominante faible
|}
Par exemple en ''do'' majeur :
* fonction de tonique : '''''do''<sup>5</sup> (C)''', ''mi''<sup>5</sup> (E–), ''la''<sup>5</sup> (A–) ;
* fonction de sous-dominante : '''''fa''<sup>5</sup> (F)''', ''ré''<sup>5</sup> (D–) ;
* fonction de dominante : '''''sol''<sup>5</sup> (G)''' ou ''sol''<sup>7</sup><sub>+</sub> (G<sup>7</sup>), ''si''<sup> <s>5</s></sup> (B<sup>o</sup>).
En ''la'' mineur harmonique :
* fonction de tonique : '''''la''<sup>5</sup> (A–)''', ''fa''<sup>5</sup> (F) [, rarement : ''do''<sup>+5</sup> (C<sup>+</sup>)] ;
* fonction de sous-dominante : '''''ré''<sup>5</sup> (D–)''', ''si''<sup> <s>5</s></sup> (B<sup>o</sup>) ;
* fonction de dominante : '''''mi''<sup>5</sup> (E)''' ou ''mi''<sup>7</sup><sub>+</sub> (E<sup>7</sup>), ''sol''♯<sup> <s>5</s></sup> (G♯<sup>o</sup>).
Le fait d'utiliser des accords différents pour remplir une fonction permet d'enrichir l'harmonie, et de jouer sur l'équilibre entre satisfaction d'une attente (on respecte les règles sur les fonctions) et surprise (mais on n'utilise pas l'accord attendu).
=== Les dominantes secondaires ===
On utilise aussi des accords de septième dominante se fondant sur un autre degré que la dominante de la gamme ; on parle de « dominante secondaire ». Typiquement, avant un accord de septième de dominante, on utilise parfois un accord de dominante de dominante, dont le degré est alors noté « {{Times New Roman|V}} de {{Times New Roman|V}} » ou « {{Times New Roman|V}}/{{Times New Roman|V}} » ; la fondamentale est de l'accord est alors situé cinq degrés au-dessus de la dominante ({{Times New Roman|V}}), c'est donc le degré {{Times New Roman|IX}}, c'est-à-dire le degré {{Times New Roman|II}} de la tonalité en cours). Ou encore, on utilise un accord de dominante du degré {{Times New Roman|IV}} (« {{Times New Roman|V}} de {{Times New Roman|IV}} », la fondamentale est alors le degré {{Times New Roman|I}}) avant un accord sur le degré {{Times New Roman|IV}} lui-même.
Par exemple, en tonalité de ''do'' majeur, on peut trouver un accord ''ré - fa''♯'' - la - do'' (chiffré {{Times New Roman|V}} de {{Times New Roman|V}}<sup>7</sup><sub>+</sub>), avant un accord ''sol - si - ré - fa'' ({{Times New Roman|V}}<sup>7</sup><sub>+</sub>). L'accord ''ré - fa''♯'' - la - do'' est l'accord de septième de dominante des tonalités de ''sol''. Dans la même tonalité, on pourra utiliser un accord ''do - mi - sol - si''♭ ({{Times New Roman|V}} de {{Times New Roman|IV}}<sup>7</sup><sub>+</sub>) avant un accord ''fa - la - do'' ({{Times New Roman|IV}}<sup>5</sup>). Le recours à une dominante secondaire peut atténuer une transition, par exemple avec un enchaînement ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> → ''fa''<sup>5</sup> (C → C<sup>7</sup> → F) qui correspond à un enchaînement {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}} : le passage ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> (C → C<sup>7</sup>) se fait en ajoutant une note (le ''si''♭) et rend naturel le passage ''do'' → ''fa''.
Sur les sept degré de la gamme, on ne considère en général que cinq dominantes secondaires : en effet, la dominante du degré {{Times New Roman|I}} est la dominante « naturelle, primaire » de la tonalité (et n'est donc pas secondaire) ; et utiliser la dominante de {{Times New Roman|VII}} consisterait à considérer l'accord de {{Times New Roman|VII}} comme un accord propre, on évite donc les « {{Times New Roman|V}} de “{{Times New Roman|V}}” » (mais les « “{{Times New Roman|V}}” de {{Times New Roman|V}} » sont tout à fait « acceptables »).
=== Enchaînements classiques ===
Nous avons donc vu que l'on trouve fréquemment les enchaînements suivants :
* pour créer une instabilité :
** {{Times New Roman|I}} → {{Times New Roman|V}},
** {{Times New Roman|I}} → {{Times New Roman|IV}} (instabilité moins forte mais incertitude sur le sens d'évolution) ;
* pour maintenir l'instabilité :
** {{Times New Roman|IV}} → {{Times New Roman|V}} ;
* pour résoudre l'instabilité :
** {{Times New Roman|IV}} → {{Times New Roman|I}},
** {{Times New Roman|V}} → {{Times New Roman|I}}, cas particuliers (voir plus bas) :
*** {{Times New Roman|V}}<sup>+4</sup> → {{Times New Roman|I}}<sup>6</sup>,
*** {{Times New Roman|I}}<sup>6</sup><sub>4</sub> → {{Times New Roman|V}}<sup>7</sup><sub>+</sub> → {{Times New Roman|I}}<sup>5</sup>.
Les degrés indiqués ci-dessus sont les fonctions ; on peut donc utiliser les substitutions suivantes :
* {{Times New Roman|I}} par {{Times New Roman|VI}} et, en tonalité majeure, {{Times New Roman|III}} ;
* {{Times New Roman|IV}} par {{Times New Roman|II}} ;
* {{Times New Roman|V}} par {{Times New Roman|VII}}.
Pour enrichir l'harmonie, on peut utiliser les dominantes secondaires, en particulier :
* {{Times New Roman|V}} de {{Times New Roman|V}} ({{Times New Roman|II}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|V}},
* {{Times New Roman|V}} de {{Times New Roman|IV}} ({{Times New Roman|I}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|IV}}.
On peut enchaîner les enchaînements, par exemple {{Times New Roman|I}} → {{Times New Roman|IV}} → {{Times New Roman|V}}, ou encore {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}}… En jazz, on utilise très fréquemment l'enchaînement {{Times New Roman|II}} → {{Times New Roman|V}} → {{Times New Roman|I}} (deux-cinq-un).
On peut bien sûr avoir d'autres enchaînements, mais ces règles permettent d'analyser un grand nombre de morceaux, et donnent des clefs utiles pour la composition. Nous voyons ci-après un certain nombre d'enchaînements courants dans différents styles
== Exercice ==
Un hautboïste travaille la sonate en ''do'' mineur S. 277 de Heinichen. Sur le deuxième mouvement ''Allegro'', il a du mal à travailler un passage en raison des altérations accidentelles. Sur la suggestion de sa professeure, il décide d'analyser la progression d'accords sous-jacente afin que les altérations deviennent logiques. Il s'agit d'un duo hautbois et basson pour lequel les accords ne sont pas chiffrés, le basson étant ici un instrument soliste et non pas un élément de la basse continue.
Sur l'extrait suivant, déterminez les basses et la qualité (chiffrage) des accords sous-jacents. Commentez.
[[Fichier:Sonate hautbois basson heinichen 2e mvt mes49.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen.]]
{{note|L'œuvre est en ''do'' mineur et devrait donc avoir trois bémols à la clef, or ici il n'y en a que deux. En effet, le ''la'' pouvant être bécarre en mode mineur mélodique ascendant, le compositeur a préféré le noter explicitement en altération accidentelle lorsque l'on est en mode mélodique naturel, harmonique ou mélodique descendant. C'est un procédé assez courant à l'époque baroque.}}
{{boîte déroulante/début|titre=Solution}}
Une des difficultés ici est que les arpèges joués par les instruments sont agrémentés de notes de passage.
Les notes de la basse (du basson) sont différentes entre le premier et le deuxième temps de chaque mesure et ne peuvent pas appartenir au même accord. On a donc un accord par temps.
Sur le premier temps de chaque mesure, le basson joue une octave. La note concernée est donc la basse de chaque accord. Pour savoir s'il s'agit d'un accord à l'état fondamental ou d'un renversement, on regarde ce que joue le hautbois : dans un mouvement conjoint (succession d'intervalles de secondes), il est difficile de distinguer les notes de l'arpège des notes de passage, mais
: les notes des grands intervalles font partie de l'accord.
Ainsi, sur le premier temps de la première mesure (la basse est un ''mi''♭), on a une sixte descendante ''sol''-''si''♭ et, à la fin du temps, une tierce descendante ''sol''-''mi''♭. L'accord est donc ''mi''♭-''sol''-''si''♭, c'est un accord de quinte (accord parfait à l'état fondamental). À la fin du premier temps, le basson joue un ''do'', c'est donc une note étrangère.
Sur le second temps de la première mesure, le basson joue une tierce ascendante ''fa''-''la''♭, la première note est la basse de l'accord et la seconde une des notes de l'accord. Le hautbois commence par une sixte descendante ''la''♭-''do'', l'accord est donc ''fa''-''la''♭-''do'', un accord de quinte (accord parfait à l'état fondamental). Le ''do'' du basson la fin du premier temps est donc une anticipation.
Les autres notes étrangères de la première mesure sont des notes de passage.
Mais il faut faire attention : en suivant ce principe, sur les premiers temps des deuxième et troisième mesure, nous aurions des accords de septième d'espèce (puisque la septième est majeure). Or, on ne trouve pas, ou alors exceptionnellement, d'accord de septième d'espèce dans le baroque, mais quasi exclusivement des accords de septième de dominante. Donc au début de la deuxième mesure, le ''la''♮ est une appoggiature du ''si''♭, l'accord est donc ''si''♭-''ré''-''fa'', un asscord de quinte. De même, au début de la troisième mesure, le ''sol'' est une appoggiature du ''la''♭.
Il faut donc se méfier d'une analyse purement « mathématique ». Il faut s'attacher à ressentir la musique, et à connaître les styles, pour faire une analyse pertinente.
Ci-dessous, nous avons grisé les notes étrangères.
[[Fichier:Sonate hautbois basson heinichen 2e mvt mes49 analyse.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen. Analyse de la progression harmonique.]]
Le chiffrage jazz équivalent est :
: | E♭ F– | B♭<sup>Δ</sup> E♭ | A♭<sup>Δ</sup> D– | G …
Nous remarquons une progression assez régulière :
: ''mi''♭ ↗[2<sup>de</sup>] ''fa'' | ↘[5<sup>te</sup>] ''si''♭ ↗[4<sup>te</sup>] ''mi''♭ | ↘[5<sup>te</sup>] ''la''♭ ↗[4<sup>te</sup>] ''ré'' | ↘[5<sup>te</sup>] ''sol''
Le ''mi''♭ est le degré {{Times New Roman|III}} de la tonalité principale (''do'' mineur), c'est donc une tonique faible ; il « joue le même rôle » qu'un ''do''. S'il y avait eu un accord de ''do'' au début de l'extrait, on aurait eu une progression parfaitement régulière ↗[4<sup>te</sup>] ↘[5<sup>te</sup>].
Nous avons les modulations suivantes :
* mesure 49 : ''do'' mineur naturel (le ''si''♭ n'est pas une sensible) avec un accord sur “{{Times New Roman|I}}” (tonique faible, {{Times New Roman|III}}, pour la première analyse, ou bien tonique forte, {{Times New Roman|I}}, pour la seconde) suivi d'un accord sur {{Times New Roman|IV}} ;
* mesure 50 : ''si''♭ majeur avec un accord sur {{Times New Roman|I}} suivi d'un accord sur {{Times New Roman|IV}} ;
* mesure 51 : ''la''♭ majeur avec un accord sur {{Times New Roman|I}}, et emprunt à ''do'' majeur avec un accord sur {{Times New Roman|II}} ({{Times New Roman|IV}} faible).
On a donc une marche harmonique {{Times New Roman|I}} → {{Times New Roman|IV}} qui descend d'une seconde majeure (un ton) à chaque mesure (''do'' → ''si''♭ → ''la''♭), avec une exception sur la dernière mesure (modulation en cours de mesure et descente d'une seconde mineure au lieu de majeure).
Ce passage est donc construit sur une régularité, une règle qui crée un effet d'attente — enchaînement {{Times New Roman|I}}<sup>5</sup> → {{Times New Roman|IV}}<sup>5</sup> avec une marche harmonique d'une seconde majeure descendante —, et des « surprises », des exceptions au début — ce n'est pas un accord {{Times New Roman|I}}<sup>5</sup> mais un accord {{Times New Roman|III}}<sup>5</sup> — et à la fin — modulation en milieu de mesure et dernière descente d'une seconde mineure (½t ''la''♭ → ''sol'').
L'extrait ne permet pas de le deviner, mais la mesure 52 est un retour en ''do'' mineur, avec donc une modulation sur la dominante (accord de ''sol''<sup>7</sup><sub>+</sub>, G<sup>7</sup>).
{{boîte déroulante/fin}}
== Progression d'accords ==
Comme pour la mélodie, la succession des accords dans un morceau, la progression d'accords, suit des règles. Et comme pour la mélodie, les règles diffèrent d'un style musical à l'autre et la créativité consiste à parfois ne pas suivre ces règles. Et comme pour la mélodie, on part d'un ensemble de notes organisé, d'une gamme caractéristique d'une tonalité, d'un mode.
Les accords les plus utilisés pour une tonalité donnée sont les accords dont la fondamentale sont les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la tonalité, en particulier la triade {{Times New Roman|I}}, appelée « accord parfait » ou « accord de tonique », et l'accord de septième {{Times New Roman|V}}, appelé « septième de dominante ».
Le fait d'avoir une progression d'accords qui se répète permet de structurer un morceau. Pour les morceaux courts, il participe au plaisir de l'écoute et facilite la mémorisation (par exemple le découpage couplet-refrain d'une chanson). Sur les morceaux longs, une trop grande régularité peut introduire de la lassitude, les longs morceaux sont souvent découpés en parties présentant chacune une progression régulière. Le fait d'avoir une progression régulière permet la pratique de l'improvisation : cadence en musique classique, solo en jazz et blues.
; Note
: Le terme « cadence » désigne plusieurs choses différentes, et notamment en harmonie :
:* une partie improvisée dans un opéra ou un concerto, sens utilisé ci-dessus ;
:* une progression d'accords pour ponctuer un morceau et en particulier pour le conclure, sens utilisé dans la section suivante.
=== Accords peu utilisés ===
En mode mineur, l'accord de quinte augmentée {{Times New Roman|III<sup>+5</sup>}} est très peu utilisé. C'est un accord dissonant ; il intervient en général comme appogiature de l'accord de tonique (par exemple en ''la'' mineur : {{Times New Roman|III<sup>+5</sup>}} ''do'' - ''mi'' - ''sol''♯ → {{Times New Roman|I<sup>6</sup>}} ''do'' - ''mi'' - ''la''), ou de l'accord de dominante ({{Times New Roman|III<sup>6</sup><sub>+3</sub>}} ''mi'' - ''sol''♯ - ''do'' → {{Times New Roman|V<sup>5</sup>}} ''mi'' - ''sol''♯ - ''si''). Il peut être aussi utilisé comme préparation à l'accord de sous-dominante (enchaînement {{Times New Roman|III}} → {{Times New Roman|IV}}). Par ailleurs, il a une constitution symétrique — c'est l'empilement de deux tierces majeures — et ses renversements ont les mêmes intervalles à l'enharmonie près (quinte augmentée/sixte mineure, tierce majeure/quarte diminuée). De ce fait, un même accord est commun, par renversement et à l'enharmonie près, à trois tonalités : le premier renversement de l'accord ''do'' - ''mi'' - ''sol''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur) est enharmonique à ''mi'' - ''sol''♯ - ''si''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''do''♯ mineur) ; le second renversement est enharmonique à ''la''♭ - ''do'' - ''mi'' ({{Times New Roman|III}}<sup>e</sup> degré de ''fa'' mineur).
=== Accords très utilisés ===
Les trois accords les plus utilisés sont les accords de tonique (degré {{Times New Roman|I}}), de sous-dominante ({{Times New Roman|IV}}) et de dominante ({{Times New Roman|V}}). Ils interviennent en particulier en fin de phrase, dans les cadences. L'accord de dominante sert souvent à introduire une modulation : la modulation commence sur l'accord de dominante de la nouvelle tonalité. On note que l'accord de sous-dominante est situé une quinte juste en dessous de la tonique, les accords de dominante et de sous-dominante sont donc symétriques.
En jazz, on utilise également très fréquemment l'accord de la sus-tonique (degré {{Times New Roman|II}}), souvent dans des progressions {{Times New Roman|II}} - {{Times New Roman|V}} (- {{Times New Roman|I}}). Rappelons que l'accord de sus-tonique a la fonction de sous-dominante.
=== Cadences et ''turnaround'' ===
Le terme « cadence » provient de l'italien ''cadenza'' et désigne la « chute », la fin d'un morceau ou d'une phrase musicale.
On distingue deux types de cadences :
* les cadences conclusive, qui créent une sensation de complétude ;
* les cadences suspensives, qui crèent une sensation d'attente.
==== Cadence parfaite ====
[[Fichier:Au clair de le lune cadence parfaite.midi|thumb|''Au clair de la lune'', harmonisé avec une cadence parfaite (italienne).]]
[[Fichier:Au clair de le lune mineur cadence parfaite.midi|thumb|''Idem'' mais en mode mineur harmonique.]]
La cadence parfaite est l'enchaînement de l'accord de dominante suivi de l'accord parfait : {{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}}, les deux accord étant à l'état fondamental. Elle donne une impression de stabilité et est donc très souvent utilisée pour conclure un morceau. C'est une cadence conclusive.
On peut aussi utiliser l'accord de septième de dominante, la dissonance introduisant une tension résolue par l'accord parfait : {{Times New Roman|V<sup>7</sup><sub>+</sub> - I<sup>5</sup>}}.
Elle est souvent précédée de l'accord construit sur le IV<sup>e</sup> degré, appelé « accord de préparation », pour former la cadence italienne : {{Times New Roman|IV<sup>5</sup> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}}.
Elle est également souvent précédée du second renversement de l'accord de tonique, qui est alors appelé « appoggiature de la cadence » : {{Times New Roman|I<sup>6</sup><sub>4</sub> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}} (on remarque que les accords {{Times New Roman|I}}<sup>6</sup><sub>4</sub> et {{Times New Roman|V}}<sup>5</sup> ont la basse en commun, et que l'on peut passer de l'un à l'autre par un mouvement conjoint sur les autres notes).
{{clear}}
==== Demi-cadence ====
[[Fichier:Au clair de le lune demi cadence.midi|thumb|''Au clair de la lune'', harmonisé avec une demi-cadence.]]
Une demi-cadence est une phrase ou un morceau se concluant sur l'accord construit sur le cinquième degré. Il provoque une sensation d'attente, de suspens. Il s'agit en général d'une succession {{Times New Roman|II - V}} ou {{Times New Roman|IV - V}}. C'est une cadence suspensive. On uilise rarement un accord de septième de dominante.
{{clear}}
==== Cadence rompue ou évitée ====
La cadence rompue, ou cadence évitée, est succession d'un accord de dominante et d'un accord de sus-dominante, {{Times New Roman|V}} - {{Times New Roman|VI}}. C'est une cadence suspensive.
==== Cadence imparfaite ====
Une cadence imparfaite est une cadence {{Times New Roman|V - I}}, comme la cadence parfaite, mais dont au moins un des deux accords est dans un état renversé.
==== Cadence plagale ====
La cadence plagale — du grec ''plagios'', oblique, en biais — est la succession de l'accord construit sur le quatrième degré, suivi de l'accord parfait : {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}. Elle peut être utilisée après une cadence parfaite ({{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}} - {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}). Elle donne un caractère solennel, voire religieux — elle est parfois appelée « cadence amen » —, elle a un côté antique qui rappelle la musique modale et médiévale<ref>{{lien web |url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/la-cadence-amen-ou-comment-se-dire-adieu-7191921 |titre=La cadence « Amen » ou comment se dire adieu |auteur=Max Dozolme (MAXXI Classique) |site=France Musique |date=2025-04-25 |consulté le=2025-04-25}}.</ref>.
C'est une cadence conclusive.
==== {{lang|en|Turnaround}} ====
[[Fichier:Au clair de le lune turnaround.midi|thumb|Au clair de la lune, harmonisé en style jazz : accords de 7{{e}}, anatole suivie d'un ''{{lang|en|turnaround}}'' ii-V-I.]]
Le terme ''{{lang|en|turnaround}}'' signifie revirement, retournement. C'est une succession d'accords que fait la transition entre deux parties, en créant une tension-résolution. Le ''{{lang|en|turnaround}}'' le plus courant est la succession {{Times New Roman|II - V - I}}.
On utilise également fréquemment l'anatole : {{Times New Roman|I - VI - II - V}}.
{| class="wikitable"
|+ Progressions typiques d'accords dans une tonalité majeure
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Cadence<br />parfaite<br /> {{Times New Roman|V - I}}
! scope="col" | Cadence<br />italienne<br />{{Times New Roman|IV - V - I}}
! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou IV - V}}
! scope="col" | Cadence<br />plagale<br />{{Times New Roman|IV - I}}
! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}}
! scope="col" | Anatole<br />{{Times New Roman|I - vi - ii - V}}
|-
|''Do'' majeur || || G - C || F - G - C || Dm - G ou F - G || F - C || Dm - G - C || C - Am - Dm - G
|-
|''Sol'' majeur || ''fa''♯ || D - G || C - D - G || Am - D ou C - D || C - G || Am - D - G || G - Em - Am - D
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || A - D || G - A - D || Em - A ou G - A || G - D || Em - A - D || D - Bm - Em - A
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || E - A || D - E - A || Bm - E ou D - E || D - A || Bm - E - A || A - F♯m - B - E
|-
| ''Fa'' majeur || ''si''♭ || C - F || B♭ - C - F || Gm - C ou B♭ - C || B♭ - F || Gm - C - F || F - Dm - Gm - C
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || F - B♭ || E♭ - F - B♭ || Cm - F ou E♭ - F || E♭ - B♭ || Cm - F - B♭ || B♭ - Gm - Cm - F
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || B♭ - E♭ || A♭ - B♭ - E♭ || Fm - B♭ ou A♭ - B♭ || A♭ - E♭ || Fm - B♭ - E♭ || Gm - Cm - Fm - B♭
|}
{| class="wikitable"
|+ Progressions typiques d'accords dans une tonalité mineure
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Cadence<br />parfaite<br />{{Times New Roman|V - i}}
! scope="col" | Cadence<br />italienne<br />{{Times New Roman|iv - V - i}}
! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou iv - V}}
! scope="col" | Cadence<br />plagale<br />{{Times New Roman|iv - i}}
! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}}
! scope="col" | Anatole<br />{{Times New Roman|i - VI - ii - V}}
|-
| ''La'' mineur<br />harmonique || || E - Am || Dm - E - Am || B° - E ou Dm - E || Dm - Am || B° - E - Am || Am - F - B° - E
|-
| ''Mi'' mineur<br />harmonique || ''fa''♯ || B - Em || Am - B - Em || F♯° - B ou Am - B || Am - Em || F♯° - B - Em || Em - C - F♯° - B
|-
| ''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || F♯ - Bm || Em - F♯ - Bm || C♯° - F♯ ou Em - F♯ || Em - Bm || C♯° - F♯ - Bm || Bm - G - C♯° - F♯
|-
| ''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || C♯ - F♯m || Bm - C♯ - F♯m || G♯° - C♯ ou Bm - C♯ || Bm - F♯m || G♯° - C♯ - F♯m || A+ - D - G♯° - C♯
|-
| ''Ré'' mineur<br />harmonique || ''si''♭ || A - Dm || Gm - A - Dm || E° - A ou Gm - A || Gm - Dm || E° - A - Dm || Dm - B♭ - E° - A
|-
| ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || D - Gm || Cm - D - Gm || A° - D ou Cm - D || Cm - Gm|| A° - D - Gm || Gm - E♭ - A° - D
|-
| ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || G - Cm || Fm - G - Cm || D° - G ou Fm - G || Fm - Dm || D° - G - Cm || Cm - A♭ - D° - G
|}
==== Exemple : ''La Mer'' ====
: {{lien web
| url = https://www.youtube.com/watch?v=PXQh9jTwwoA
| titre = Charles Trenet - La mer (Officiel) [Live Version]
| site = YouTube
| auteur = Charles Trenet
| consulté le = 2020-12-24
}}
Le début de ''La Mer'' (Charles Trenet, 1946) est en ''do'' majeur et est harmonisé par l'anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (C - Am - Dm - G<sup>7</sup>) sur deux mesures, jouée deux fois ({{Times New Roman|1=<nowiki>|I-vi|ii-V</nowiki><sup>7</sup><nowiki>|</nowiki>}} × 2). Viennent des variations avec les progressions {{Times New Roman|I-III-vi-V<sup>7</sup>}} (C - E - Am - G<sup>7</sup>) puis la « progression ’50s » (voir plus bas) {{Times New Roman|I-vi-IV-VI<sup>7</sup>}} (C - Am - F - A<sup>7</sup>, on remarque que {{Times New Roman|IV}}/F est le relatif majeur du {{Times New Roman|ii}}/Dm de l'anatole), jouées chacune une fois sur deux mesure ; puis cette première partie se conclut par une demie cadence {{Times New Roman|ii-V<sup>7</sup>}} sur une mesure puis une dernière anatole sur trois mesures ({{Times New Roman|1=<nowiki>|I-vi|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}}). Cela constitue une première partie « A » sur douze mesures qui se termine par une demi-cadence ({{Times New Roman|ii-V<sup>7</sup>}}) qui appelle une suite. Cette partie A est jouée une deuxième fois mais la fin est modifiée pour la transition : les deux dernières mesures {{Times New Roman|<nowiki>|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}} deviennent {{Times New Roman|<nowiki>|ii-V</nowiki><sup>7</sup><nowiki>|I|</nowiki>}} (|Dm-G7|C|), cette partie « A’ » se conclut donc par une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}).
Le morceau passe ensuite en tonalité de ''mi'' majeur, donc une tierce au dessus de ''do'' majeur, sur six mesures. Cette partie utilise une progression ’50s {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (E - C♯m - A - B<sup>7</sup>), qui est rappelons-le une variation de l'anatole, l'accord {{Times New Roman|ii}} (Fm) étant remplacé par son relatif majeur {{Times New Roman|IV}} (A). Cette anatole modifiée est jouée deux fois puis la partie en ''mi'' majeur se conclut par l'accord parfait {{Times New Roman|I}} joué sur deux mesures (|E|E|), on a donc, avec la mesure précédente, avec une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}).
Suivent ensuite six mesures en ''sol'' majeur, donc à nouveau une tierce au dessus de ''mi'' majeur. Elle comporte une progression {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (G - Em - C - D<sup>7</sup>), donc anatole avec substitution du {{Times New Roman|ii}}/Am par son relatif majeur {{Times New Roman|VI}}/C (progression ’50s), puis une anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (G - Em - Am - D<sup>7</sup>) et deux mesure sur la tonique {{Times New Roman|I-I<sup>7</sup>}} (G - G<sup>7</sup>), formant à nouveau une cadence parfaite. La fin sur un accord de septième, dissonant, appelle une suite.
Cette partie « B » de douze mesures comporte donc deux parties similaires « B1 » et « B2 » qui forment une marche harmonique (montée d'une tierce).
Le morceau se conclut par une reprise de la partie « A’ » et se termine donc par une cadence parfaite.
Nous avons une structure A-A’-B-A’ sur 48 mesures, proche la forme AABA étudiée plus loin.
Donc ''La Mer'' est un morceau structuré autour de l'anatole avec des variations (progression ’50s, substitution du {{Times New Roman|ii}} par son relatif majeur {{Times New Roman|IV}}) et comportant une marche harmonique dans sa troisième partie. Les parties se concluent par des ''{{lang|en|turnarounds}}'' sous la forme d'une cadence parfaite ou, pour la partie A, par une demi-cadence.
{| border="1" rules="rows" frame="hsides"
|+ Structure de ''La Mer''
|- align="center"
|
| colspan="12" | ''do'' majeur
|
|- align="center"
! scope="row" rowspan=2 | A
| colspan="2" | anatole
| colspan="2" | //
| colspan="2" | variation
| colspan="2" | ’50s
| ½ c.
| colspan="3" | anatole
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii}} || <nowiki>|</nowiki> {{Times New Roman|V<sup>7</sup>}} || <nowiki>|</nowiki>
|- align="center"
! scope="row" rowspan="2" | A’
| colspan="2" | anatole
| colspan="2" | //
| colspan="2" | variation
| colspan="2" | ’50s
| ½ c.
| colspan="2" | anatole
| c.p.
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki>
|- align="center"
|
| colspan="6" | B1 : ''mi'' majeur
| colspan="6" background="lightgray" | B2 : ''sol'' majeur
|
|- align="center"
! scope="row" rowspan="2" | B
| colspan="2" | ’50s
| colspan="2" | //
|colspan="2" | c.p.
| colspan="2" | ’50s
| colspan="2" | //
|colspan="2" | c.p.
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I<sup>7</sup>}} || <nowiki>|</nowiki>
|-
! scope="row" | A’
| colspan="12" |
|
|}
=== Progression blues ===
La musique blues est apparue dans les années 1860. Elle est en général bâtie sur une grille d'accords ''({{lang|en|changes}})'' immuable de douze mesures ''({{lang|en|twelve-bar blues}})''. C'est sur cet accompagnement qui se répète que s'ajoute la mélodie — chant et solo. Cette structure est typique du blues et se retrouve dans ses dérivés comme le rock 'n' roll.
Le rythme est toujours un rythme ternaire syncopé ''({{lang|en|shuffle, swing, groove}}, ''notes inégales'')'' : la mesure est à quatre temps, mais la noire est divisée en noire-croche en triolet, ou encore triolet de croche en appuyant la première et la troisième.
La mélodie se construit en général sur une gamme blues de six degrés (gamme pentatonique mineure avec une quarte augmentée), mais bien que la gamme soit mineure, l'harmonie est construite sur la gamme majeure homonyme : un blues en ''fa'' a une mélodie sur la gamme de ''fa'' mineur, mais une harmonie sur la gamme de ''fa'' majeur. La grille d'accord comporte les accords construits sur les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la gamme majeure homonyme. Les accords sont souvent des accords de septième (donc avec une tierce majeure et une septième mineure), il ne s'agit donc pas d'une harmonisation de gamme diatonique (puisque la septième est majeure sur l'accord de tonique).
Par exemple, pour un blues en ''do'' :
* accord parfait de do majeur, C ({{Times New Roman|I}}<sup>er</sup> degré) ;
* accord parfait de fa majeur, F ({{Times New Roman|IV}}<sup>e</sup> degré) ;
* accord parfait de sol majeur, G ({{Times New Roman|V}}<sup>e</sup> degré).
Il existe quelques morceaux harmonisés avec des accords mineurs, comme par exemple ''As the Years Go Passing By'' d'Albert King (Duje Records, 1959).
La progression blues est organisée en trois blocs de quatre mesures ayant les fonctions suivantes (voir ci-dessus ''[[#Harmonie fonctionnelle|Harmonie fonctionnelle]]'') :
* quatre mesures toniques ;
* quatre mesures sous-dominantes ;
* quatre mesures dominantes.
La forme la plus simple, que Jeff Gardner appelle « forme A », est la suivante :
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, forme A
|-
! scope="row" | Tonique
| width="50px" | I
| width="50px" | I
| width="50px" | I
| width="50px" | I
|-
! scope="row" | Sous-domminante
| width="50px" | IV
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
! scope="row" | Dominante
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | V
|}
La progression {{Times New Roman|I-V}} des deux dernières mesures forment le ''{{lang|en|turnaround}}'', la demie cadence qui lance le cycle suivant. Nous présentons ci-dessous un exemple typique de ligne de basse ''({{lang|en|walking bass}})'' pour le ''{{lang|en|turnaround}}'' d'un blues en ''la'' :
[[Fichier:Turnaround classique blues en la.svg|Exemple typique de ligne de basse pour un ''turnaround'' de blues en ''la''.]]
[[Fichier:Blues mi harmonie elementaire.midi|thumb|Blues en ''mi'', harmonisé de manière élémentaire avec une ''{{lang|en|walking bass}}''.]]
Vous pouvez écouter ci-contre une harmonisation typique d'un blues en ''mi''. Les accords sont exécutés par une basse marchante ''({{lang|en|walking bass}})'', qui joue une arpège sur la triade avec l'ajout d'une sixte majeure et d'une septième mineure, et par une guitare qui joue un accord de puissance ''({{lang|en|power chord}})'', qui n'est composé que de la fondamentale et de la quinte juste, avec une sixte en appoggiature.
La forme B s'obtient en changeant la deuxième mesure : on joue un degré {{Times New Roman|IV}} au lieu d'un degré {{Times New Roman|I}}. La progression {{Times New Roman|I-IV}} sur les deux premières mesures est appelé ''{{lang|en|quick change}}''.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, forme B
|-
| width="50px" | I
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
| width="50px" | IV
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | V
|}
Par exemple, ''Sweet Home Chicago'' (Robert Johnson, 1936) est un blues en ''fa'' ; sa grille d'accords, aux variations près, suit une forme B :
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression de ''Sweet Home Chicago''
|-
| width="50px" | F
| width="50px" | B♭
| width="50px" | F
| width="50px" | F
|-
| width="50px" | B♭
| width="50px" | B♭
| width="50px" | F
| width="50px" | F
|-
| width="50px" | C7
| width="50px" | B♭7
| width="50px" | F7
| width="50px" | C7
|}
: Écouter {{lien web
| url =https://www.youtube.com/watch?v=dkftesK2dck
| titre = Robert Johnson "Sweet Home Chicago"
| auteur = Michal Angel
| site = YouTube
| date = 2007-12-09 | consulté le = 2020-12-17
}}
Les formes C et D s'obtiennent à partir des formes A et B en changeant le dernier accord par un accord sur le degré {{Times New Roman|I}}, ce qui forme une cadence plagale.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, formes C et D
|-
| colspan="4" | …
|-
| colspan="4" | …
|-
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|}
L'harmonie peut être enrichie, notamment en jazz. Voici par exemple une grille du blues souvent utilisés en bebop.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Exemple de progression de blues bebop sur une base de forme B
|-
| width="60px" | I<sup>7</sup>
| width="60px" | IV<sup>7</sup>
| width="60px" | I<sup>7</sup>
| width="60px" | V–<sup>7</sup> <nowiki>|</nowiki> I<sup>7</sup>
|-
| width="60px" | IV<sup>7</sup>
| width="60px" | IV<sup>7</sup>
| width="60px" | I<sup>7</sup>
| width="60px" | VI<sup>7 ♯9 ♭13</sup>
|-
| width="60px" | II–<sup>7</sup>
| width="60px" | V<sup>7</sup>
| width="60px" | V<sup>7</sup> <nowiki>|</nowiki> IV<sup>7</sup>
| width="60px" | II–<sup>7</sup> <nowiki>|</nowiki> V<sup>7</sup>
|}
On peut aussi trouver des blues sur huit mesures, sur seize mesures comme ''Watermelon Man'' de Herbie Hancock (album ''Takin' Off'', Blue Note, 1962) ou ''Let's Dance'' de Jim Lee (interprété par Chris Montez, Monogram, 1962)
* {{lien web
|url= https://www.dailymotion.com/video/x5iduwo
|titre=Herbie Hancock - Watermelon Man (1962)
|auteur=theUnforgettablesTv
|site=Dailymotion
|date=2003 |consulté le=2021-02-09
}}
* {{lien web
|url=https://www.youtube.com/watch?v=6JXshurYONc
|titre=Let's Dance
|auteur=Chris Montez
|site=YouTube
|date=2016-08-06 |consulté le=2021-02-09
}}
À l'inverse, certains blues peuvent avoir une structure plus simple que les douze mesure ; par exemple ''Hoochie Coochie Man'' de Willie Dixon (interprété par Muddy Waters sous le titre ''Mannish Boy'', Chicago Blues, 1954) est construit sur un seul accord répété tout le long de la chanson.
* {{lien web
|url=https://www.dailymotion.com/video/x5iduwo
|titre=Muddy Waters - Hoochie Coochie Man
|auteur=Muddy Waters
|site=Dailymotion
|date=2012 | consulté le=2021-02-09
}}
=== Cadence andalouse ===
La cadence andalouse est une progression de quatre accords, descendant par mouvement conjoint :
* en mode de ''mi'' (mode phrygien) : {{Times New Roman|IV}} - {{Times New Roman|III}} - {{Times New Roman|II}} - {{Times New Roman|I}} ;<br />par exemple en ''mi'' phrygien : Am - G - F - E ; en ''do'' phrygien : Fm - E♭ - D♭ - C ;<br />on notera que le degré {{Times New Roman|III}} est diésé dans l'accord final (ou bécarre s'il est bémol dans la tonalité) ;
* en mode mineur : {{Times New Roman|I}} - {{Times New Roman|VII}} - {{Times New Roman|VI}} - {{Times New Roman|V}} ;<br />par exemple en ''la'' mineur : Am - G - F - E ; en ''do'' mineur : Cm - B♭ - A♭ - m ;<br />comme précédemment, on notera que le degré {{Times New Roman|VII}} est diésé dans l'accord final.
=== Progressions selon le cercle des quintes ===
[[Fichier:Cercle quintes degres tonalite majeure.svg|vignette|Cercle des quinte justes (parcouru dans le sens des aiguilles d'une montre) des degrés d'une tonalité majeure.]]
La progression {{Times New Roman|V-I}} est la cadence parfaite, mais on peut aussi l'employer au milieu d'un morceau. Cette progression étant courte, sa répétition crée de la lassitude ; on peut la compléter par d'autres accords séparés d'une quinte juste, en suivant le « cercle des quintes » : {{Times New Roman|I-V-IX}}, la neuvième étant enharmonique de la seconde, on obtient {{Times New Roman|I-V-II}}.
On peut continuer de décrire le cercle des quintes : {{Times New Roman|I-V-II-VI}}, on obtient l'anatole dans le désordre ; on peut à l'inverse étendre les quintes vers la gauche, {{Times New Roman|IV-I-V-II-VI}}.
En musique populaire, on trouve fréquemment une progression fondée sur les accord {{Times New Roman|I}}, {{Times New Roman|IV}}, {{Times New Roman|V}} et {{Times New Roman|VI}}, popularisée dans les années 1950. La « progression années 1950 », « progression ''{{lang|en|fifties ('50)}}'' » ''({{lang|en|'50s progression}})'' est dans l'ordre {{Times New Roman|I-VI-IV-V}}. On trouve aussi cette progression en musique classique. Si la tonalité est majeure, la triade sur la sus-dominante est mineure, les autres sont majeures, on notera donc souvent {{Times New Roman|I-vi-IV-V}}. On peut avoir des permutations circulaires (le dernier accord venant au début, ou vice-versa) : {{Times New Roman|vi-IV-V-I}}, {{Times New Roman|IV-V-I-vi}} et {{Times New Roman|V-I-vi-IV}}.
{| class="wikitable"
|+ Accords selon la tonalité
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" style="font-family:Times New Roman" | I
! scope="col" style="font-family:Times New Roman" | IV
! scope="col" style="font-family:Times New Roman" | V
! scope="col" style="font-family:Times New Roman" | vi
|-
|''Do'' majeur || || C || F || G || Am
|-
|''Sol'' majeur || ''fa''♯ || G || C || D || Em
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || D || G || A || Bm
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A || D || E || F♯m
|-
| ''Fa'' majeur || ''si''♭ || F || B♭ || C || Dm
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭ || E♭ || F || Gm
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭ || A♭ || B♭ || Cm
|}
Par exemple, en tonalité de ''do'' majeur, la progression {{Times New Roman|I-vi-IV-V}} sera C-Am-F-G.
Il existe d'autres progressions utilisant ces accords mais dans un autre ordre, typiquement {{Times New Roman|I–IV–vi–V}} ou une de ses permutations circulaires : {{Times New Roman|IV–vi–V-I}}, {{Times New Roman|vi–V-I-IV}} ou {{Times New Roman|V-I-IV-vi}}. Ou dans un autre ordre.
PV Nova l'illustre dans plusieurs de ses « expériences » dans la version {{Times New Roman|vi-V-IV-I}}, soit Am-G-F-C, ou encore {{Times New Roman|vi-IV-I-V}}, soit Am-F-C-G :
: {{lien web
| url = https://www.youtube.com/watch?v=w08LeZGbXq4
| titre = Expérience n<sup>o</sup> 6 — La Happy Pop
| auteur = PV Nova
| site = YouTube
| date = 2011-08-20 | consulté le = 2020-12-13
}}
et cela devient un gag récurrent avec son « chapeau des accords magiques qu'on nous ressort à toutes les sauces »
: {{lien web
| url = https://www.youtube.com/watch?v=VMY_vc4nZAU
| titre = Expérience n<sup>o</sup> 14 — La Soupe dou Brasil
| auteur = PV Nova
| site = YouTube
| date = 2012-10-03 | consulté le = 2020-12-17
}}
Cette récurrence est également parodiée par le groupe The Axis of Awesome avec ses « chansons à quatre accords » ''({{lang|en|four-chords song}})'', dans une sketch où ils mêlent 47 chansons en utilisant l'ordre {{Times New Roman|I-V-vi-IV}} :
: {{lien web
| url = https://www.youtube.com/watch?v=oOlDewpCfZQ
| titre = 4 Chords | Music Videos | The Axis Of Awesome
| auteur = The Axis of Awesome
| site = YouTube
| date = 2011-07-20 | consulté le = 2020-12-17
}}
{{boîte déroulante/début|titre=Chansons mêlées dans le sketch}}
# Journey : ''Don't Stop Believing'' ;
# James Blunt : ''You're Beautiful'' ;
# Black Eyed Peas : ''Where Is the Love'' ;
# Alphaville : ''Forever Young'' ;
# Jason Mraz : ''I'm Yours'' ;
# Train : ''Hey Soul Sister'' ;
# The Calling : ''Wherever You Will Go'' ;
# Elton John : ''Can You Feel The Love Tonight'' (''Le Roi lion'') ;
# Akon : ''Don't Matter'' ;
# John Denver : ''Take Me Home, Country Roads'' ;
# Lady Gaga : ''Paparazzi'' ;
# U2 : ''With Or Without You'' ;
# The Last Goodnight : ''Pictures of You'' ;
# Maroon Five : ''She Will Be Loved'' ;
# The Beatles : ''Let It Be'' ;
# Bob Marley : ''No Woman No Cry'' ;
# Marcy Playground : ''Sex and Candy'' ;
# Men At Work : ''Land Down Under'' ;
# thème de ''America's Funniest Home Videos'' (équivalent des émissions ''Vidéo Gag'' et ''Drôle de vidéo'') ;
# Jack Johnson : ''Taylor'' ;
# Spice Girls : ''Two Become One'' ;
# A Ha : ''Take On Me'' ;
# Green Day : ''When I Come Around'' ;
# Eagle Eye Cherry : ''Save Tonight'' ;
# Toto : ''Africa'' ;
# Beyonce : ''If I Were A Boy'' ;
# Kelly Clarkson : ''Behind These Hazel Eyes'' ;
# Jason DeRulo : ''In My Head'' ;
# The Smashing Pumpkins : ''Bullet With Butterfly Wings'' ;
# Joan Osborne : ''One Of Us'' ;
# Avril Lavigne : ''Complicated'' ;
# The Offspring : ''Self Esteem'' ;
# The Offspring : ''You're Gonna Go Far Kid'' ;
# Akon : ''Beautiful'' ;
# Timberland featuring OneRepublic : ''Apologize'' ;
# Eminem featuring Rihanna : ''Love the Way You Lie'' ;
# Bon Jovi : ''It's My Life'' ;
# Lady Gaga : ''Pokerface'' ;
# Aqua : ''Barbie Girl'' ;
# Red Hot Chili Peppers : ''Otherside'' ;
# The Gregory Brothers : ''Double Rainbow'' ;
# MGMT : ''Kids'' ;
# Andrea Bocelli : ''Time To Say Goodbye'' ;
# Robert Burns : ''Auld Lang Syne'' ;
# Five for fighting : ''Superman'' ;
# The Axis of Awesome : ''Birdplane'' ;
# Missy Higgins : ''Scar''.
{{boîte déroulante/fin}}
Vous pouvez par exemple jouer les accords C-G-Am-F ({{Times New Roman|I-V-vi-IV}}) et chanter dessus ''{{lang|en|Let It Be}}'' (Paul McCartney, The Beattles, 1970) ou ''Libérée, délivrée'' (Robert Lopez, ''La Reine des neiges'', 2013).
La progression {{Times New Roman|I-V-vi-IV}} est considérée comme « optimiste » tandis que sa variante {{Times New Roman|iv-IV-I-V}} est considérée comme « pessimiste ».
On peut voir la progression {{Times New Roman|I-vi-IV-V}} comme une variante de l'anatole {{Times New Roman|I-vi-ii-V}}, obtenue en remplaçant l'accord de sustonique {{Times New Roman|ii}} par l'accord de sous-dominante {{Times New Roman|IV}} (son relatif majeur, et degré ayant la même fonction).
==== Exemples de progression selon le cercle des quintes en musique classique ====
[[Fichier:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.midi|vignette|Dietrich Buxtehude, Psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]]
Cette progression selon la cercle des quintes, sous la forme {{Times New Roman|I-vi-IV-V}}, apparaît déjà au {{pc|xvii}}<sup>e</sup> siècle dans le psaume 42 ''Quem ad modum desiderat cervis'' (BuxVW92) de Dietrich Buxtehude (1637-1707). Le morceau est en ''fa'' majeur, la progression d'accords est donc F-Dm-B♭-C.
: {{lien web
| url = https://www.youtube.com/watch?v=8FmV9l1RqSg
| titre = D. Buxtehude - Quemadmodum desiderat cervus, BuxWV 92
| auteur = Longobardo
| site = YouTube
| date = 2013-04-06 | consulté la = 2021-01-01
}}
[[File:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.svg|vignette|450x450px|center|Dietrich Buxtehude, psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]]
{{clear}}
[[Fichier:JSBach BWV140 cantate 4 mesures.midi|vignette|J.-S. Bach, cantate BWV140, quatre premières mesures.]]
On la trouve également dans l'ouverture de la cantate ''{{lang|de|Wachet auf, ruft uns die Stimme}}'' de Jean-Sébastien Bach (BWV140, 1731). Le morceau est en ''mi''♭ majeur, la progression d'accords est donc E♭-Cm-A♭<sup>6</sup>-B♭.
[[Fichier:JSBach BWV140 cantate 4 mesures.svg|vignette|center|J.-S. Bach, cantate BWV140, quatre premières mesures.|alt=|517x517px]]
{{clear}}
[[Fichier:Mozart K310 Sonate8 mesures 45 a 49.midi|vignette|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]]
La même progression est utilisée par Mozart, par exemple dans le premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778), la progression d'accords est C-Am-F-G qui correspond à la progression {{Times New Roman|III-i-VI-VII}} de ''la'' mineur, mais à la progression {{Times New Roman|I-vi-IV-V}} de la gamme relative, ''do'' majeur .
[[Fichier:Mozart K310 Sonate8 mesures 45 a 49.svg|vignette|center|500px|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]]
=== Substitution tritonique ===
Un des accords les plus utilisés est donc l'accord de septième de dominante, {{Times New Roman|V<sup>7</sup><sub>+</sub>}} qui contient les degrés {{Times New Roman|V}}, {{Times New Roman|VII}}, {{Times New Roman|II}} ({{Times New Roman|IX}}) et {{Times New Roman|IV}}({{Times New Roman|XI}}) ; par exemple, en tonalité de ''do'' majeur, l'accord de ''sol'' septième (G<sup>7</sup>) contient les notes ''sol''-''si''-''ré''-''fa''. Si l'on prend l'accord dont la fondamentale est trois tons (triton) au-dessus ou en dessous — l'octave contenant six tons, on arrive sur la même note —, {{Times New Roman|♭II<sup>7</sup>}}, ici ''ré''♭ septième (D♭<sup>7</sup>), celui-ci contient les notes ''ré''♭-''fa''-''la''♭-''do''♭, cette dernière note étant l'enharmonique de ''si''. Les deux accords G<sup>7</sup> et D♭<sup>7</sup> ont donc deux notes en commun : le ''fa'' et le ''si''/''do''♭.
Il est donc fréquent en jazz de substituer l'accord {{Times New Roman|V<sup>7</sup><sub>+</sub>}} par l'accord {{Times New Roman|♭II<sup>7</sup>}}. Par exemple, la progression {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|V<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}} devient {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|♭II<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}}. C'est un procédé courant de réharmonisation (le fait de remplacer un accord par un autre dans un morceau existant).
Les six substitutions possibles sont donc : C<sup>7</sup>↔F♯<sup>7</sup> - D♭<sup>7</sup>↔G<sup>7</sup> - D<sup>7</sup>↔A♭<sup>7</sup> - E♭<sup>7</sup>↔A<sup>7</sup> - E<sup>7</sup>↔B♭<sup>7</sup> - F<sup>7</sup>↔B<sup>7</sup>.
[[Fichier:Übermäsiger Terzquartakkord.jpg|vignette|Exemple de cadence parfaite en ''do'' majeur avec substitution tritonique (sixte française).]]
Dans l'accord D♭<sup>7</sup>, si l'on remplace le ''do''♭ par son ''si'' enharmonique, on obtient un accord de sixte augmentée : ''ré''♭-''fa''-''la''♭-''si''. Cet accord est utilisé en musique classique depuis la Renaissance ; on distingue en fait trois accords de sixte augmentée :
* sixte française ''ré''♭-''fa''-''sol''-''si'' ;
* sixte allemande : ''ré''♭-''fa''-''la''♭-''si'' ;
* sixte italienne : ''ré''♭-''fa''-''si''.
Par exemple, le ''Quintuor en ''ut'' majeur'' de Franz Schubert (1828) se termine par une cadence parfaite dont l'accord de dominante est remplacé par une sixte française ''ré''♭-''fa''-''si''-''sol''-''si'' (''ré''♭ aux violoncelles, ''fa'' à l'alto, ''si''-''sol'' aux seconds violons et ''si'' au premier violon).
[[Fichier:Schubert C major Quintet ending.wav|vignette|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]]
[[Fichier:Schubert C major Quintet ending.png|vignette|center|upright=2.5|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]]
=== Autres accords de substitution ===
Substituer un accord consiste à utiliser un accord provenant d'une tonalité étrangère à la tonalité en cours. À la différence d'une modulation, la substitution est très courte et ne donne pas l'impression de changer de tonalité ; on a juste un sentiment « étrange » passager. Un court passage dans une autre tonalité est également appelée « emprunt ».
Nous avons déjà vu plusieurs méthodes de substitution :
* utilisation d'une note étrangère : une note étrangère — note de passage, appoggiature, anticipation, retard… — crée momentanément un accord hors tonalité ; en musique classique, ceci n'est pas considéré comme un accord en propre, mais en jazz, on parle « d'accord de passage » et « d'accord suspendu » ;
* utilisation d'une dominante secondaire : l'accord de dominante secondaire est hors tonalité ; le but ici est de faire une cadence parfaite, mais sur un autre degré que la tonique de la tonalité en cours ;
* la substitution tritonique, vue ci-dessus, pour remplacer un accord de septième de dominante.
Une dernière méthode consiste à remplacer un accord par un accord d'une gamme de même tonique, mais d'un autre mode ; on « emprunte » ''({{lang|en|borrow}})'' l'accord d'un autre mode. Par exemple, substituer un accord de la tonalité de ''do'' majeur par un accord de la tonalité de ''do'' mineur ou de ''do'' mode de ''mi'' (phrygien).
Donc en ''do'' majeur, on peut remplacer un accord de ''ré'' mineur septième (D<sub>m</sub><sup>7</sup>) par un accord de ''ré'' demi-diminué (D<sup>⌀</sup>, D<sub>m</sub><sup>7♭5</sup>) qui est un accord appartenant à la donalité de ''la'' mineur harmonique.
=== Forme AABA ===
La forme AABA est composée de deux progressions de huit mesures, notées A et B ; cela représente trente-deux mesures au total, on parle donc souvent en anglais de la ''{{lang|en|32-bars form}}''. C'est une forme que l'on retrouve dans de nombreuses chanson de comédies musicales de Broadway comme ''Have You Met Miss Jones'' (''{{lang|en|I'd Rather Be Right}}'', 1937), ''{{lang|en|Over the Rainbow}}'' (''Le Magicien d'Oz'', Harold Harlen, 1939), ''{{lang|en|All the Things You Are}}'' (''{{lang|en|Very Warm for may}}'', 1939).
Par exemple, la version de ''{{lang|en|Over the Rainbow}}'' chantée par Judy Garland est en ''la''♭ majeur et la progression d'accords est globalement :
* A (couplet) : A♭-Fm | Cm-A♭ | D♭ | Cm-A♭ | D♭ | D♭-F | B♭-E♭ | A♭
* B (pont) : A♭ | B♭m | Cm | D♭ | A♭ | B♭-G | Cm-G | B♭m-E♭
soit en degrés :
* A : {{Times New Roman|<nowiki>I-vi | iii-I | IV | iii-IV | IV | IV-vi | II-V | I</nowiki>}}
* B : {{Times New Roman|<nowiki>I | ii | iii | IV | I | II-VII | iii-VII | ii-V</nowiki>}}
Par rapport aux paroles de la chanson, on a
* A : couplet 1 ''« {{lang|en|Somewhere […] lullaby}} »'' ;
* A : couplet 2 ''« {{lang|en|Somewhere […] really do come true}} »'' ;
* B : pont ''« {{lang|en|Someday […] you'll find me}} »'' ;
* A : couplet 3 ''« {{lang|en|Somewhere […] oh why can't I?}} »'' ;
: {{lien web
| url = https://www.youtube.com/watch?v=1HRa4X07jdE
| titre = Judy Garland - Over The Rainbow (Subtitles)
| site = YouTube
| auteur = Overtherainbow
| consulté le = 2020-12-17
}}
Une mise en œuvre de la forme AABA couramment utilisée en jazz est la forme anatole (à le pas confondre avec la succession d'accords du même nom), en anglais ''{{lang|en|rythm changes}}'' car elle s'inspire du morceau ''{{lang|en|I Got the Rythm}}'' de George Gerschwin (''Girl Crazy'', 1930) :
* A : {{Times New Roman|I–vi–ii–V}} (succession d'accords « anatole ») ;
* B : {{Times New Roman|III<sup>7</sup>–VI<sup>7</sup>–II<sup>7</sup>–V<sup>7</sup>}} (les fondamentales forment une succession de quartes, donc parcourent le « cercle des quintes » à l'envers).
Par exemple, ''I Got the Rythm'' étant en ''ré''♭ majeur, la forme est :
* A : D♭ - B♭m - E♭m - A♭
* B : F7 - B♭7 - E♭7 - A♭7
=== Exemples ===
==== Début du Largo de la symphonie du Nouveau Monde ====
[[File:Largo nouveau monde 5 1res mesures.svg|vignette|Partition avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]]
[[File:Largo nouveau monde 5 1res mesures.midi|vignette|Fichier son avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]]
Nous avons reproduit ci-contre les cinq premières mesure du deuxième mouvement Largo de la symphonie « Du Nouveau Monde » (symphonie n<sup>o</sup> 9 d'Antonín Dvořák, 1893). Cliquez sur l'image pour l'agrandir.
Vous pouvez écouter cette partie jouée par un orchestre symphonique :
* {{lien web
|url =https://www.youtube.com/watch?v=y2Nw9r-F_yQ?t=565
|titre = Dvorak Symphony No.9 "From the New World" Karajan 1966
|site=YouTube (Seokjin Yoon)
|consulté le=2020-12-11
}} (à 9 min 25), par le Berliner Philharmoniker, dirigé par Herbert von Karajan (1966) ;
* {{lien web
|url = https://www.youtube.com/watch?v=ASlch7R1Zvo
|titre=Dvořák: Symphony №9, "From The New World" - II - Largo
|site=YouTube (diesillamusicae)
|consulté le=2020-12-11
}} : Wiener Philharmoniker, dirigé par Herbert von Karajan (1985).
{{clear}}
Cette partie fait intervenir onze instruments monodiques (ne jouant qu'une note à la fois) : des vents (trois bois, sept cuivres) et une percussion. Certains de ces instruments sont transpositeurs (les notes sur la partition ne sont pas les notes entendues). Jouées ensemble, ces onze lignes mélodiques forment des accords.
Pour étudier cette partition, nous réécrivons les parties des instruments transpositeurs en ''do'' et les parties en clef d’''ut'' en clef de ''fa''. Nous regroupons les parties en clef de ''fa'' d'un côté et les parties en clef de ''sol'' d'un autre.
{{boîte déroulante|Résultat|contenu=[[File:Largo nouveau monde 5 1res mesures transpositeurs en do.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde, en do.]]}}
Nous pouvons alors tout regrouper sous la forme d'un système de deux portées clef de ''fa'' et clef de ''sol'', comme une partition de piano.
{{boîte déroulante|Résultat|contenu=
[[Fichier:Largo nouveau monde 5 1res mesures accords.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords.]]
}}
[[Fichier:Largo nouveau monde 5 1res mesures accords.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords.]]
{{clear}}
Ensuite, nous ne gardons que la basse et les notes médium. Nous changeons éventuellement certaines notes d'octave afin de n'avoir que des superpositions de tierce ou de quinte (état fondamental des accords, en faisant ressortir les notes manquantes).
{{boîte déroulante|Résultat|contenu=
[[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords simplifiés.]]
}}
[[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords simplifiés.]]
Vous pouvez écouter cette partie jouée par un quintuor de cuivres (trompette, bugle, cor, trombone, tuba), donc avec des accords de cinq notes :
: {{lien web
|url=https://www.youtube.com/watch?v=pWfe60nbvjA
|titre = Largo from The New World Symphony by Dvorak
|site=YouTube (The Chamberlain Brass)
|consulté le=2020-12-11
}} : The American Academy of Arts & Letters in New York City (2017).
Nous allons maintenant chiffrer les accords.
Pour établir la basse chiffrée, il nous faut déterminer le parcours harmonique. Pour le premier accord, les tonalités les plus simples avec un ''sol'' dièse sont ''la'' majeur et ''fa'' dièse mineur ; comme le ''mi'' est bécarre, nous retenons ''la'' majeur, il s'agit donc d'un accord de quinte sur la dominante (les accords de dominante étant très utilisés, cela nous conforte dans notre choix). Puis nous avons un ''si'' bémol, nous pouvons être en ''fa'' majeur ou en ''ré'' mineur ; nous retenons ''fa'' majeur, c'est donc le renversement d'un accord sur le degré {{Times New Roman|II}}.
Dans la deuxième mesure, nous revenons en ''la'' majeur, puis, avec un ''la'' et un ''ré'' bémols, nous sommes en ''la'' bémol majeur ; nous avons donc un accord de neuvième incomplet sur la sensible, ou un accord de onzième incomplet sur la dominante.
Dans la troisième mesure, nous passons en ''ré'' majeur, avec un accord de dominante. Puis, nous arrivons dans la tonalité principale, avec le renversement d'un accord de dominante sans tierce suivi d'un accord de tonique. Nous avons donc une cadence parfaite, conclusion logique d'une phrase.
La progression des accords est donc :
{| class="wikitable"
! scope="row" | Tonalité
| ''la'' M - ''fa'' M || ''la'' M - ''la''♭ M || ''ré'' M - ''ré''♭ M || ''ré''♭ M
|-
! scope="row" | Accords
| {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|II}}<sup>6</sup><sub>4</sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|“V”}}<sup>9</sup><sub><s>5</s></sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|V}}<sup>+4</sup> || {{Times New Roman|I}}<sup>5</sup>
|}
Dans le chiffrage jazz, nous avons donc :
* une triade de ''mi'' majeur, E ;
* une triade de ''sol'' majeur avec un ''ré'' en basse : G/D ;
* à nouveau un E ;
* un accord de ''sol'' neuvième diminué incomplet, avec un ''ré'' bémol en basse : G dim<sup>9</sup>/D♭ ;
* un accord de ''la'' majeur, A ;
* un accord de ''la'' bémol septième avec une ''sol'' bémol à la basse : A♭<sup>7</sup>/G♭ ;
* la partie se conclue par un accord parfait de ''ré''♭ majeur, D♭.
Soit une progression E - G/D | E - G dim<sup>9</sup>/D♭ | A - A♭<sup>7</sup>/G♭ | D♭.
[[Fichier:Largo nouveau monde 5 1res mesures accords chiffres.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde en accords simplifiés.]]
{{clear}}
==== Thème de Smoke on the Water ====
Le morceau ''Smoke on the Water'' du groupe Deep Purple (album ''Machine Head'', 1972) possède un célèbre thème, un riff ''({{lang|en|rythmic figure}})'', joué à la guitare sous forme d'accords de puissance ''({{lang|en|power chords}})'', c'est-à-dire des accords sans tierce. Le morceau est en tonalité de ''sol'' mineur naturel (donc avec un ''fa''♮) avec ajout de la note bleue (''{{lang|en|blue note}}'', quinte diminuée, ''ré''♭), et les accords composant le thème sont G<sup>5</sup>, B♭<sup>5</sup>, C<sup>5</sup> et D♭<sup>5</sup>, ce dernier accord étant l'accord sur la note bleue et pouvant être considéré comme une appoggiature (indiqué entre parenthèse ci-après). On a donc ''a priori'', sur les deux premières mesures, une progression {{Times New Roman|I-III-IV}} puis {{Times New Roman|I-III-(♭V)-IV}}. Durant la majeure partie du thème, la guitare basse tient la note ''sol'' en pédale.
{{note|En jazz, la qualité « <sup>5</sup> » indique que l'on n'a que la quinte (et donc pas la tierce), contrairement à la notation de basse chiffrée.}}
: {{lien web
| url = https://www.dailymotion.com/video/x5ili04
| titre = Deep Purple — Smoke on the Water (Live at Montreux 2006)
| auteur = Deep Purple
| site = Dailymotion
| date = 2016 | consulté le = 2020-12-31
}}
Cependant, cette progression forme une mélodie, on peut donc plus la voir comme un contrepoint, la superposition de deux voies ayant un mouvement conjoint, joué par un seul instrument, la guitare, la voie 2 étant jouée une quarte juste en dessous de la voie 1 (la quarte juste descendante étant le renversement de la quinte juste ascendante) :
* voie 1 (aigu) : | ''sol'' - ''si''♭ - ''do'' | ''sol'' - ''si''♭ - (''ré''♭) - ''do'' | ;
* voie 2 (grave) : | ''ré'' - ''fa'' - ''sol'' | ''ré'' - ''fa'' - (''la''♭) - ''sol'' |.
En se basant sur la basse (''sol'' en pédale), nous pouvons considérer que ces deux mesures sont accompagnées d'un accord de Gm<sup>7</sup> (''sol''-''si''♭-''ré''-''fa''), chaque accord de la mélodie comprenant à chaque fois au moins une note de cet accord à l'exception de l'appogiature.
{| class="wikitable"
|+ Mise en évidence des notes de l'accord Gm<sup>7</sup>
|-
! scope="row" | Accords
| G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup>
|-
! scope="row" | Voie 1
| '''''sol''''' || '''''si''♭''' || ''do''
|-
! scope="row" | Voie 2
| '''''ré''''' || '''''fa''''' || '''''sol'''''
|-
! scope="row" | Basse
| '''''sol''''' || '''''sol''''' || '''''sol'''''
|}
Sur les deux mesures suivantes, la basse varie et suit les accords de la guitare avec un retard sur le dernier accord :
{| class="wikitable"
|+ Voies sur les mesure 3-4 du thème
|-
! scope="row" | Accords
| G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> || B♭<sup>5</sup> || G<sup>5</sup>
|-
! scope="row" | Voie 1
| ''sol'' || ''si''♭ || ''do'' || ''si''♭ || ''sol''
|-
! scope="row" | Voie 2
| ''ré'' || ''fa'' || ''sol'' || ''fa'' || ''ré''
|-
! scope="row" | Basse
| ''sol'' || ''sol'' || ''do'' || ''si''♭ || ''si''♭-''sol''
|}
Le couplet de cette chanson est aussi organisé sur une progression de quatre mesures, la guitare faisant des arpèges sur les accords G<sup>5</sup> (''sol''-''ré''-''sol'') et F<sup>5</sup> (''fa''-''do''-''fa'') :
: | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-F<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> |
soit une progression {{Times New Roman|<nowiki>| I-I | I-I | I-VII | I-I |</nowiki>}}. Nous pouvons aussi harmoniser le riff du thème sur cette progression, avec un accord F (''fa''-''la''-''do'') ; nous pouvons aussi nous rappeler que l'accord sur le degré {{Times New Roman|VII}} est plus volontiers considéré comme un accord de septième de dominante {{Times New Roman|V<sup>7</sup>}}, soit ici un accord Dm<sup>7</sup> (''ré''-''fa''-''la''-''do''). On peut donc considérer la progression harmonique sur le thème :
: | Gm-Gm | Gm-Gm | Gm-F ou Dm<sup>7</sup> | Gm-Gm |.
Cette analyse permet de proposer une harmonisation enrichie du morceau, tout en se rappelant qu'une des forces du morceau initial est justement la simplicité de sa structure, qui fait ressortir la virtuosité des musiciens. Nous pouvons ainsi comparer la version album à la version concert avec orchestre ou à la version latino de Pat Boone. À l'inverse, le groupe Psychostrip, dans une version grunge, a remplacé les accords par une ligne mélodique :
* le thème ne contient plus qu'une seule voie (la guitare ne joue pas des accords de puissance) ;
* dans les mesures 9 et 10, la deuxième guitare joue en contrepoint de type mouvement inverse, qui est en fait la voie 2 jouée en miroir ;
* l'arpège sur le couplet est remplacé par une ligne mélodique en ostinato sur une gamme blues.
{| class="wikitable"
|+ Contrepoint sur les mesures 9 et 10
|-
! scope="row" | Guitare 1
| ''sol'' ↗ ''si''♭ ↗ ''do''
|-
! scope="row" | Guitare 2
| ''sol'' ↘ ''fa'' ↘ ''ré''
|}
* {{lien web
| url = https://www.dailymotion.com/video/x5ik234
| titre = Deep Purple — Smoke on the Water (In Concert with the London Symphony Orchestra, 1999)
| auteur = Deep Purple
| site = Dailymotion
| date = 2016 | consulté le = 2020-12-31
}}
* {{lien web
| url = https://www.youtube.com/watch?v=MtUuNzVROIg
| titre = Pat Boone — Smoke on the Water (In a Metal Mood, No More Mr. Nice Guy, 1997)
| auteur = Orrore a 33 Giri
| site = YouTube
| date = 2019-06-24 | consulté le = 2020-12-31
}}
* {{lien web
| url = https://www.youtube.com/watch?v=n7zLlZ8B0Bk
| titre = Smoke on the Water (Heroes, 1993)
| auteur = Psychostrip
| site = YouTube
| date = 2018-06-20 | consulté le = 2020-12-31
}}
== Accords et improvisation ==
Nous avons vu précédemment (chapitre ''[[../Gammes et intervalles#Modes et improvisation|Gammes et intervalles > Modes et improvisation]]'') que le choix d'un mode adapté permet d'improviser sur un accord. L'harmonisation des gammes permet, en inversant le processus, d'étendre notre palette : il suffit de repérer l'accord sur une harmonisaiton de gamme, et d'utiliser cette gamme-là, dans le mode correspondant du degré de l'accord (voir ci-dessus ''[[#Harmonisation par des accords de septième|Harmonisation par des accords de septième]]'').
Par exemple, nous avons vu que l'accord sur le septième degré d'une gamme majeure était un accord demi-diminué ; nous savons donc que sur un accord demi-diminué, nous pouvons improviser sur le mode correspondant au septième degré, soit le mode de ''si'' (locrien).
Un accord de septième de dominante étant commun aux deux tonalités homonymes (par exemple ''fa'' majeur et ''fa'' mineur pour un ''do''<sup>7</sup><sub>+</sub> / C<sup>7</sup>), nous pouvons utiliser le mode de ''sol'' de la gamme majeure (mixolydien) ou de la gamme mineure mineure (mode phrygien dominant, ou phrygien espagnol) pour improviser. Mais l'accord de septième de dominante est aussi l'accord au début d'une grille blues ; on peut donc improviser avec une gamme blues, même si la tierce est majeure dans l'accord et mineure dans la gamme.
[[Fichier:Mode improvisation accords do complet.svg]]
== Autres accords courants ==
[[fichier:Cluster cdefg.png|vignette|Agrégat ''do - ré - mi - fa - sol''.]]
Nous avons vu précédemment l'harmonisation des tonalités majeures et mineures harmoniques par des triades et des accords de septième ; certains accords étant rarement utilisés (l'accord sur le degré {{Times New Roman|III}} et, pour les tonalités mineures harmoniques, l'accord sur la tonique), certains accords étant utilisés comme des accords sur un autre degré (les accords sur la sensible étant considérés comme des accords de dominante sans fondamentale).
Dans l'absolu, on peut utiliser n'importe quelle combinaison de notes, jusqu'aux agrégats, ou ''{{lang|en|clusters}}'' (mot anglais signifiant « amas », « grappe ») : un ensemble de notes contigües, séparées par des intervalles de seconde. Dans la pratique, on reste souvent sur des accords composés de superpositions de tierces, sauf dans le cas de transitions (voir la section ''[[#Notes étrangères|Notes étrangère]]'').
=== En musique classique ===
On utilise parfois des accords dont les notes ne sont pas dans la tonalité (hors modulation). Il peut s'agir d'accords de passage, de notes étrangères, par exemple utilisant un chromatisme (mouvement conjoint par demi-tons).
Outre les accords de passage, les autres accords que l'on rencontre couramment en musique classique sont les accords de neuvième, et les accords de onzième et treizième sur tonique. Ces accords sont simplement obtenus en continuant à empiler les tierces. Il n'y a pas d'accord d'ordre supérieur car la quinzième est deux octaves au-dessus de la fondamentale.
Comme pour les accords de septième, on distingue les accords de neuvième de dominante et les accords de neuvième d'espèce. Dans le cas de la neuvième de dominante, il y a une différence entre les tonalités majeures et mineures : l'intervalle de neuvième est respectivement majeur et mineur. Les chiffrages des renversements peuvent donc différer. Comme pour les accords de septième de dominante, on considère que les accords de septième sur le degré {{Times New Roman|VI}} sont en fait des accords de neuvième de dominante sans fondamentale.
Les accords de neuvième d'espèce sont en général préparés et résolus. Préparés : la neuvième étant une note dissonante (c'est à une octave près la seconde de la fondamentale), l'accord qui précède doit contenir cette note, mais dans un accord consonant ; la neuvième est donc commune avec l'accord précédent. Résolus : la dissonance est résolue en abaissant la neuvième par un mouvement conjoint. Par exemple, en tonalité de ''do'' majeur, si l'on veut utiliser un accord de neuvième d'espèce sur la tonique ''(do - mi - sol - si - ré)'', on peut utiliser avant un accord de dominante ''(sol - si - ré)'' en préparation puis un accord parfait sur le degré {{Times New Roman|IV}} ''(fa - la - do)'' en résolution ; nous avons donc sur la voie la plus aigüe la succession ''ré'' (consonant) - ''ré'' (dissonant) - ''do'' (consonant).
On rencontre également parfois des accords de onzième et de treizième. On omet en général la tierce, car elle est dissonante avec la onzième. L'accord le plus fréquemment rencontré est l'accord sur la tonique : on considère alors que c'est un accord sur la dominante que l'on a enrichi « par le bas », en ajoutant une quinte inférieure. par exemple, dans la tonalité de ''do'' majeur, l'accord ''do - sol - si - ré - fa'' est considéré comme un accord de septième de dominante sur tonique, le degré étant noté « {{Times New Roman|V}}/{{Times New Roman|I}} ». De même pour l'accord ''do - sol - si - ré - fa - la'' qui est considéré comme un accord de neuvième de dominante sur tonique.
=== En jazz ===
En jazz, on utilise fréquemment l'accord de sixte à la place de l'accord de septième majeure sur la tonique. Par exemple, en ''do'' majeur, on utilise l'accord C<sup>6</sup> ''(do - mi - sol - la)'' à la place de C<sup>Δ</sup> ''(do - mi - sol - si)''. On peut noter que C<sup>6</sup> est un renversement de Am<sup>7</sup> et pourrait donc se noter Am<sup>7</sup>/C ; cependant, le fait de le noter C<sup>6</sup> indique que l'on a bien un accord sur la tonique qui s'inscrit dans la tonalité de ''do'' majeur (et non, par exemple, de ''la'' mineur naturelle) — par rapport à l'harmonie fonctionnelle, on remarquera que Am<sup>7</sup> a une fonction tonique, l'utilisation d'un renversement de Am<sup>7</sup> à la place d'un accord de C<sup>Δ</sup> est donc logique.
Les accords de neuvième, onzième et treizième sont utilisés comme accords de septième enrichis. Le chiffrage suit les règles habituelles : on ajoute un « 9 », un « 11 » ou un « 13 » au chiffrage de l'accord de septième.
On utilise également des accords dits « suspendus » : ce sont des accords de transition qui sont obtenus en prenant une triade majeure ou mineure et en remplaçant la tierce par la quarte juste (cas le plus fréquent) ou la seconde majeure. Plus particulièrement, lorsque l'on parle simplement « d'accord suspendu » sans plus de précision, cela désigne l'accord de neuvième avec une quarte suspendue, noté « 9sus4 » ou simplement « sus ».
== L'harmonie tonale ==
L'harmonie tonale est un ensemble de règle assez strictes qui s'appliquent dans la musique savante européenne, de la période baroque à la période classique classique ({{pc|xiv}}<sup>e</sup>-{{pc|xviii}}<sup>e</sup> siècle). Certaines règles sont encore largement appliquées dans divers styles musicaux actuels, y compris populaire (rock, rap…), d'autres sont au contraire ignorées (par exemple, un enchaînement de plusieurs accords de même qualité forme un mouvement parallèle, ce qui est proscrit en harmonie tonale). De nos jours, on peut voir ces règles comme des règles « de bon goût », et leur application stricte comme une manière de composer « à la manière de ».
Précédemment, nous avons vu la progression des accords. Ci-après, nous abordons aussi la manière dont les notes de l'accord sont réparties entre plusieurs voix, et comment on construit chaque voix.
=== Concepts fondamentaux ===
; Consonance
: Les intervalles sont considérés comme « plus ou moins consonants » :
:* consonance parfaite : unisson, quinte et octave ;
:* consonance mixte (parfaite dans certains contextes, imparfaite dans d'autres) : quarte ;
:* consonance imparfaite : tierce et sixte ;
:* dissonance : seconde et septième.
; Degrés
: Certains degrés sont considérés comme « forts », « meilleurs », ce sont les « notes tonales » : {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (sous-dominante) et {{Times New Roman|V}} (dominante).
[[Fichier:Mouvements harmoniques.svg|vignette|upright=0.75|Mouvements harmoniques.]]
; Mouvements
: Le mouvement décrit la manière dont les voix évoluent les unes par rapport aux autres :
:# Mouvement parallèle : les voix sont séparées par un intervalle constant.
:# Mouvement oblique : une voix reste constante, c'est le bourdon ; l'autre monte ou descend.
:# Mouvement contraire : une voix descend, l'autre monte.
:# Échange de voix : les voix échangent de note ; les mélodies se croisent mais on a toujours le même intervalle harmonique.
{{clear}}
=== Premières règles ===
; Règle du plus court chemin
: Quand on passe d'un accord à l'autre, la répartition des notes se fait de sorte que chaque voix fait le plus petit mouvement possible. Notamment : si les deux accords ont des notes en commun, alors les voix concernées gardent la même note.
: Les deux voix les plus importantes sont la voix aigüe — soprano — et la voix la plus grave — basse. Ces deux voix sont relativement libres : la voix de soprano a la mélodie, la voix de basse fonde l'harmonie. La règle du plus court chemin s'applique surtout aux voix intermédiaires ; si l'on a des mouvements conjoints, ou du moins de petits intervalles — c'est le sens de la règle du plus court chemin —, alors les voix sont plus faciles à interpréter. Cette règle évite également que les voix n'empiètent l'une sur l'autre (voir la règle « éviter le croisement des voix »).
; Éviter les consonances parfaites consécutives
:* Lorsque deux voix sont à l'unisson ou à l'octave, elles ne doivent pas garder le même intervalle, l'effet serait trop plat.
:* Lorsque deux voix sont à la quarte ou à la quinte, elles ne doivent pas garder le même intervalle, car l'effet est trop dur.
: Pour éviter cela, lorsque l'on part d'un intervalle juste, on a intérêt à pratiquer un mouvement contraire aux voix qui ne gardent pas la même note, ou au moins un mouvement direct : les voix vont dans le même sens, mais l'intervalle change.
: Notez que même avec le mouvement contraire, on peut avoir des consonances parfaites consécutives, par exemple si une voix fait ''do'' aigu ↗ ''sol'' aigu et l'autre ''sol'' médium ↘ ''do'' grave.
: L'interdiction des consonances parfaites consécutives n'a pas été toujours appliquée, le mouvement parallèle strict a d'ailleurs été le premier procédé utilisé dans la musique religieuse au {{pc|x}}<sup>e</sup> siècle. On peut par exemple utiliser des quintes parallèles pour donner un style médiéval au morceau. On peut également utiliser des octaves parallèles sur plusieurs notes afin de créer un effet de renforcement de la mélodie.
: Par ailleurs, les consonances parfaites consécutives sont acceptées lorsqu'il s'agit d'une cadence (transition entre deux parties ou bien conclusion du morceau).
; Éviter le croisement des voix
: Les voix sont organisées de la plus grave à la plus aigüe. Deux voix n'étant pas à l'unisson, celle qui est plus aigüe ne doit pas devenir la plus grave et ''vice versa''.
; Soigner la partie soprano
: Comme c'est celle qu'on entend le mieux, c'est en général celle qui porte la mélodie principale. On lui applique des règles spécifiques :
:# Si elle chante la sensible dans un accord de dominante ({{Times New Roman|V}}), alors elle doit monter à la tonique, c'est-à-dire que la note suivante sera la tonique située un demi-ton au dessus.
:# Si l'on arrive à une quinte ou une octave entre les parties basse et soprano par un mouvement direct, alors sur la partie soprano, le mouvement doit être conjoint. On doit donc arriver à cette situation par des notes voisines au soprano.
; Préférer certains accords
: Les deux degrés les plus importants sont la tonique ({{Times New Roman|I}}) et la dominante ({{Times New Roman|V}}), les accords correspondants ont donc une importance particulière.
: À l'inverse, l'accord de sensible ({{Times New Roman|VII}}) n'est pas considéré comme ayant une fonction harmonique forte. On le considère comme un accord de dominante affaibli. En tonalité mineure, on évite également l'accord de médiante ({{Times New Roman|III}}).
: Donc on utilise en priorité les accords de :
:# {{Times New Roman|I}} et {{Times New Roman|V}}.
:# Puis {{Times New Roman|II}}, {{Times New Roman|IV}}, {{Times New Roman|VI}} ; et {{Times New Roman|III}} en mode majeur.
:# On évite {{Times New Roman|VII}} ; et {{Times New Roman|III}} en mode mineur.
; Préférer certains enchaînements
: Les enchaînements d'accord peuvent être classés par ordre de préférence. Par ordre de préférence décroissante (du « meilleur » au « moins bon ») :
:# Meilleurs enchaînements : quarte ascendante ou descendante. Notons que la quarte est le renversement de la quinte, on a donc des enchaînements stables et naturels, mais avec un intervalle plus court qu'un enchaînement de quintes.
:# Bons enchaînements : tierce ascendante ou descendante. Les accords consécutifs ont deux notes en commun.
:# Enchaînements médiocres : seconde ascendante ou descendante. Les accords sont voisins, mais ils n'ont aucune note en commun. On les utilise de préférence en mouvement ascendant, et on utilise surtout les enchaînements {{Times New Roman|IV}}-{{Times New Roman|V}}, {{Times New Roman|V}}-{{Times New Roman|VI}} et éventuellement {{Times New Roman|I}}-{{Times New Roman|II}}.
:# Les autres enchaînements sont à éviter.
: On peut atténuer l'effet d'un enchaînement médiocre en plaçant le second accord sur un temps faible ou bien en passant par un accord intermédiaire.
[[Fichier:Progression Vplus4 I6.svg|thumb|Résolution d'un accord de triton (quarte sensible) vers l'accord de sixte de la tonique.]]
; La septième descend par mouvement conjoint
: Dans un accord de septième de dominante, la septième — qui est donc le degré {{Times New Roman|IV}} — descend par mouvement conjoint — elle est donc suivie du degré {{Times New Roman|III}}.
: Corolaire : un accord {{Times New Roman|V}}<sup>+4</sup> se résout par un accord {{Times New Roman|I}}<sup>6</sup> : on a bien un enchaînement {{Times New Roman|V}} → {{Times New Roman|I}}, et la 7{{e}} (degré {{Times New Roman|IV}}), qui est la basse de l'accord {{Times New Roman|V}}<sup>+4</sup>, descend d'un degré pour donner la basse de l'accord {{Times New Roman|I}}<sup>6</sup> (degré {{Times New Roman|III}}).
{{clear}}
[[Fichier:Progression I64 V7plus I5.svg|thumb|Accord de sixte et de quarte cadentiel.]]
; Un accord de sixte et quarte est un accord de passage
: Le second renversement d'un accord parfait est soit une appoggiature, soit un accord de passage, soit un accord de broderie.
: S'il s'agit de l'accord de tonique {{Times New Roman|I}}<sup>6</sup><sub>4</sub>, c'est « accord de sixte et quarte de cadence », l'appoggiature de l'accord de dominante de la cadence parfaite.
{{clear}}
Mais il faut appliquer ces règles avec discernement. Par exemple, la voix la plus aigüe est celle qui s'entend le mieux, c'est donc elle qui porte la mélodie principale. Il est important qu'elle reste la plus aigüe. La voix la plus grave porte l'harmonie, elle pose les accords, il est donc également important qu'elle reste la plus grave. Ceci a deux conséquences :
# Ces deux voix extrêmes peuvent avoir des intervalles mélodiques importants et donc déroger à la règle du plus court chemin : la voix aigüe parce que la mélodie prime, la voix de basse parce que la progression d'accords prime.
# Les croisements des voix intermédiaires sont moins critiques.
Par ailleurs, si l'on applique strictement toutes les règles « meilleurs accords, meilleurs enchaînements », on produit un effet conventionnel, stéréotypé. Il est donc important d'utiliser les solutions « moins bonnes », « médiocres » pour apporter de la variété.
Ajoutons que les renversements d'accords permettent d'avoir plus de souplesse : on reste sur le même accord, mais on enrichit la mélodie sur chaque voix.
Le ''Bolero'' de Maurice Ravel (1928) brise un certain nombre de ces règles. Par exemple, de la mesure 39 à la mesure 59, la harpe joue des secondes. De la mesure 149 à la mesure 165, les piccolo jouent à la sixte, dans des mouvement strictement parallèle, ce qui donne d'ailleurs une sonorité étrange. À partir de la mesure 239, de nombreux instruments jouent en mouvement parallèles (piccolos, flûtes, hautbois, cor, clarinettes et violons).
=== Application ===
[[Fichier:Harmonisation possible de frere jacques exercice.svg|vignette|Exercice : harmoniser ''Frère Jacques''.]]
Harmoniser ''Frère Jacques''.
Nous considérons un morceau à quatre voix : basse, ténor, alto et soprano. La soprano chante la mélodie de ''Frère Jacques''. L'exercice consiste à proposer l'écriture des trois autres voix en respectant les règles énoncées ci-dessus. Pour simplifier, nous ajoutons les contraintes suivantes :
* toutes les voix chantent des blanches ;
* nous nous limitons aux accords de quinte (accords de trois sons composés d'une tierce et d'une quinte) sans avoir recours à leurs renversements (accords de sixte, accords de sixte et de quarte).
Les notes à gauche de la portée indiquent la tessiture (ou ambitus), l'amplitude que peut chanter la voix.
{{clear}}
{{boîte déroulante/début|titre=Solution possible}}
[[Fichier:Harmonisation possible de frere jacques solution.svg|vignette|Harmonisation possible de ''Frère Jacques'' (solution de l'exercice).]]
Il n'y a pas qu'une solution possible.
Le premier accord doit contenir un ''do''. Nous sommes manifestement en tonalité de ''do'' majeur, nous proposons de commencer par l'accord parfait de ''do'' majeur, I<sup>5</sup>.
Le deuxième accord doit comporter un ''ré''. Si nous utilisons l'accord de quinte de ''ré'', nous allons créer une quinte parallèle. Nous pourrions utiliser un renversement, mais nous nous imposons de chercher un autre accord. Il peut s'agir de l'accord ''si''<sup>5</sup> ''(si-ré-fa)'' ou de l'accord de ''sol''<sup>5</sup> ''(sol-si-ré)''. La dernière solution permet d'utiliser l'accord de dominante qui est un accord important de la tonalité. La règle du plus court chemin imposerait le ''sol'' grave pour la partie de basse, mais cela est proche de la limite du chanteur, nous préférons passer au ''sol'' aigu, plus facile à chanter. Nous vérifions qu'il n'y a pas de quinte parallèle : l'intervalle ascendant ''do-sol'' (basse-alto) devient ''sol-si'' (3<sup>ce</sup>), l'intervalle descendant ''do-sol'' (soprano-alto) devient ''ré-si'' (3<sup>ce</sup>).
De la même manière, pour le troisième accord, nous ne pouvons pas passer à un accord de ''la''<sup>5</sup> pour éviter une quinte parallèle. Nous avons le choix entre ''do''<sup>5</sup> ''(do-mi-sol)'' et ''mi''<sup>5</sup> ''(mi-sol-si)''. Nous préférons revenir à l'accord de fondamental, solution très stable (l'enchaînement {{Times New Roman|V}}-{{Times New Roman|I}} formant une cadence parfaite).
Pour le quatrième accord, nous pourrions rester sur l'accord parfait de ''do'' mais cela planterait en quelque sorte la fin du morceau puisque l'on resterait sur la cadence parfaite ; or, nous connaissons le morceau et savons qu'il n'est pas fini. Nous choisissons l'accord de ''la''<sup>5</sup> qui est une sixte ascendante ({{Times New Roman|I}}-{{Times New Roman|VI}}).
Nos aurions pu répartir les voix différemment. Par exemple :
* alto : ''sol''-''si''-''sol''-''do'' ;
* ténor : ''mi''-''ré''-''mi''-''mi''.
{{boîte déroulante/fin}}
[[Fichier:Harmonisation possible de frere jacques.midi|vignette|Fichier son correspondant.]]
{{clear}}
== Annexe ==
=== Accords en musique classique ===
Un accord est un ensemble de notes jouées simultanément. Il peut s'agir :
* de notes jouées par plusieurs instruments ;
* de notes jouées par un même instrument : piano, clavecin, orgue, guitare, harpe (la plupart des instruments à clavier et des instruments à corde).
Pour deux notes jouées simultanément, on parle d'intervalle « harmonique » (par opposition à l'intervalle « mélodique » qui concerne les notes jouées successivement).
Les notes répétées à différentes octaves ne changent pas la nature de l'accord.
La musique classique considère en général des empilements de tierces ; un accord de trois notes sera constitué de deux tierces successives, un accord de quatre notes de trois tierces…
Lorsque tous les intervalles sont des intervalles impairs — tierces, quintes, septièmes, neuvièmes, onzièmes, treizièmes… — alors l'accord est dit « à l'état fondamental » (ou encore « primitif » ou « direct »). La note de la plus grave est appelée « fondamentale » de l'accord. Lorsque l'accord comporte un ou des intervalles pairs, l'accord est dit « renversé » ; la note la plus grave est appelée « basse ».
De manière plus générale, l'accord est dit à l'état fondamental lorsque la basse est aussi la fondamentale. On a donc un état idéal de l'accord (état canonique) — un empilement strict de tierces — et l'état réel de l'accord — l'empilement des notes réellement jouées, avec d'éventuels redoublements, omissions et inversions ; et seule la basse indique si l'accord est à l'état fondamental ou renversé.
Le chiffrage dit de « basse continue » ''({{lang|it|basso continuo}})'' désigne la représentation d'un accord sous la forme d'un ou plusieurs chiffres arabes et éventuellement d'un chiffre romain.
==== Accords de trois notes ====
En musique classique, les seuls accords considérés comme parfaitement consonants, c'est-à-dire sonnant agréablement à l'oreille, sont appelés « accords parfaits ». Si l'on prend une tonalité et un mode donné, alors l'accord construit par superposition es degrés I, III et V de cette gamme porte le nom de la gamme qui l'a généré.
[[fichier:Accord do majeur chiffre.svg|vignette|upright=0.5|Accord parfait de ''do'' majeur chiffré.]]
Par exemple :
* « l'accord parfait de ''do'' majeur » est composé des notes ''do'', ''mi'' et ''sol'' ;
* « l'accord parfait de ''la'' mineur » est composé des notes ''la'', ''do'' et ''mi''.
Un accord parfait majeur est donc composé, en partant de la fondamentale, d'une tierce majeure et d'une quinte juste. Un accord parfait mineur est composé d'une tierce mineure et d'une quinte juste.
L'accord parfait à l'état fondamental est appelé « accord de quinte » et est simplement chiffré « 5 » pour indiquer la quinte.
On peut également commencer un accord sur sa deuxième ou sa troisième note, en faisant monter celle(s) qui précède(nt) à l'octave suivante. On parle alors de « renversement d'accord » ou d'accord « renversé ».
[[Fichier:Accord do majeur renversements chiffre.svg|vignette|upright=0.75|Accord parfait de ''do'' majeur et ses renversements, chiffrés.]]
Par exemple,
* le premier renversement de l'accord parfait de ''do'' majeur est :<br /> ''mi'', ''sol'', ''do'' ;
* le second renversement de l'accord parfait de do majeur est :<br /> ''sol'', ''do'', ''mi''.
Les notes conservent leur nom de « fondamentale », « tierce » et « quinte » malgré le changement d'ordre. La note la plus grave est appelée « basse ».
Dans le cas du premier renversement, le deuxième note est la tierce de la basse (la note la plus grave) et la troisième note est la sixte ; le chiffrage en chiffres arabes est donc « 6 » (puisque l'on omet la tierce) et l'accord est appelé « accord de sixte ». Pour le deuxième renversement, les intervalles sont la quarte et la sixte, le chiffrage est donc « 6-4 » et l'accord est appelé « accord de sixte et de quarte ».
Dans tous les cas, on chiffre le degré on considérant la fondamentale, par exemple {{Times New Roman|I}} si l'accord est construit sur la tonique de la gamme.
Les autres accords de trois notes que l'on rencontre sont :
* l'accord de quinte diminuée, constitué d'une tierce mineure et d'une quinte diminuée ; lorsqu'il est construit sur le septième degré d'une gamme, on considère que c'est un accord de septième de dominante sans fondamentale (voir plus bas), le degré est donc indiqué « “{{Times New Roman|V}}” » (cinq entre guillemets) et non « {{Times New Roman|VII}} » ;
* l'accord de quinte augmenté : il est composé d'une tierce majeure et qu'une quinte augmentée.
Dans le tableau ci-dessous,
* « m » désigne un intervalle mineur ;
* « M » un intervalle majeur ou le mode majeur ;
* « J » un intervalle juste ;
* « d » un intervalle diminué ;
* « A » un intervalle augmenté ;
* « mh » le mode mineur harmonique ;
* « ma » le mode mineur ascendant ;
* « md » le mode mineur descendant.
{| class="wikitable"
|+ Accords de trois notes
! scope="col" rowspan="2" | Nom
! scope="col" rowspan="2" | 3<sup>ce</sup>
! scope="col" rowspan="2" | 5<sup>te</sup>
! scope="col" rowspan="2" | État fondamental
! scope="col" rowspan="2" | 1<sup>er</sup> renversement
! scope="col" rowspan="2" | 2<sup>nd</sup> renversement
! scope="col" colspan="4"| Construit sur les degrés
|-
! scope="col" | M
! scope="col" | mh
! scope="col" | ma
! scope="col" | md
|-
| Accord parfait<br /> majeur || M || J
| accord de quinte || accord de sixte || accord de<br />sixte et de quarte
| {{Times New Roman|I, IV, V}} || {{Times New Roman|V, VI}} || {{Times New Roman|IV, V}} || {{Times New Roman|III, VI, VII}}
|-
| Accord parfait<br /> mineur || m || J
| accord de quinte || accord de sixte || accord de<br />sixte et de quarte
| {{Times New Roman|II, III, VI}} || {{Times New Roman|I, IV}} || {{Times New Roman|I, II}} || {{Times New Roman|I, IV, V}}
|-
| Accord de<br />quinte diminuée || m || d
| accord de<br />quinte diminuée || accord de<br />sixte sensible<br />sans fondamentale || accord de triton<br />sans fondamentale
| {{Times New Roman|VII (“V”)}} || {{Times New Roman|II, VII (“V”)}} || {{Times New Roman|VI, VII (“V”)}} || {{Times New Roman|II}}
|-
| Accord de<br />quinte augmentée || M || A
| accord de<br />quinte augmentée || accord de sixte<br />et de tierce sensible || accord de sixte et de quarte<br />sur sensible
| || {{Times New Roman|III}} || {{Times New Roman|III}} ||
|}
==== Accords de quatre notes ====
Les accords de quatre notes sont des accord composés de trois tierces superposées. La dernière note étant le septième degré de la gamme, on parle aussi d'accords de septième.
Ces accords sont dissonants : ils contiennent un intervalle de septième (soit une octave montante suivie d'une seconde descendante). Ils laissent donc une impression de « tension ».
Il existe sept différents types d'accords, ou « espèces ». Citons l'accord de septième de dominante, l'accord de septième mineure et l'accord de septième majeure.
===== L'accord de septième de dominante =====
[[Fichier:Accord 7e dominante do majeur renversements chiffre.svg|vignette|Accord de septième de dominante de ''do'' majeur et ses renversements, chiffrés.]]
L'accord de septième de dominante est l'empilement de trois tierces à partir de la dominante de la gamme, c'est-à-dire du {{Times New Roman|V}}<sup>e</sup> degré. Par exemple, l'accord de septième de dominante de ''do'' majeur est l'accord ''sol''-''si''-''ré''-''fa'', et l'accord de septième de dominante de ''la'' mineur est ''mi''-''sol''♯-''si''-''ré''. L'accord de septième de dominante dont la fondamentale est ''do'' (''do''-''mi''-''sol''-''si''♭) appartient à la gamme de ''fa'' majeur.
Que le mode soit majeur ou mineur, il est composé d'une tierce majeure, d'une quinte juste et d'une septième mineure (c'est un accord parfait majeur auquel on ajoute une septième mineure). C'est de loin l'accord de septième le plus utilisé ; il apparaît au {{pc|xvii}}<sup>e</sup> en musique classique.
Dans son état fondamental, son chiffrage est {{Times New Roman|V 7/+}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}). Le signe plus indique la sensible.
Son premier renversement est appelé « accord de quinte diminuée et sixte » et est noté {{Times New Roman|V 6/<s>5</s>}} (ou {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}).
Son deuxième renversement est appelé « accord de sixte sensible », puisque la sixte de l'accord est la sensible de la gamme, et est noté {{Times New Roman|V +6}} (ou {{Times New Roman|V<sup>+6</sup>}}).
Son troisième renversement est appelé « accord de quarte sensible » et est noté {{Times New Roman|V +4}} (ou {{Times New Roman|V<sup>+4</sup>}}).
[[Fichier:Accord 7e dominante sans fondamentale do majeur renversements chiffre.svg|vignette|Accord de septième de dominante sans fondamentale de ''do'' majeur et ses renversements, chiffrés.]]
On utilise aussi l'accord de septième de dominante sans fondamentale ; c'est alors un accord de trois notes.
Dans son état fondamental, c'est un « accord de quinte diminuée » placé sur le {{Times New Roman|VII}}<sup>e</sup> degré (mais c'est bien un accord construit sur le {{Times New Roman|V}}<sup>e</sup> degré), noté {{Times New Roman|“V” <s>5</s>}} (ou {{Times New Roman|“V”<sup><s>5</s></sup>}}). Notez les guillemets qui indiquent que la fondamentale V est absente.
Dans son premier renversement, c'est un « accord de sixte sensible sans fondamentale » noté {{Times New Roman|“V” +6/3}} (ou {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}}).
Dans son second renversement, c'est un « accord de triton sans fondamentale » (puisque le premier intervalle est une quarte augmentée qui comporte trois tons) noté {{Times New Roman|“V” 6/+4}} (ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}).
Notons qu'un accord de septième de dominante n'a pas toujours la dominante pour fondamentale : tout accord composé d'une tierce majeure, d'une quinte juste et d'une septième mineure est un accord de septième de dominante et est chiffré {{Times New Roman|<sup>7</sup><sub>+</sub>}}, quel que soit le degré sur lequel il est bâti (certaines notes peuvent avoir une altération accidentelle).
===== Les accords de septième d'espèce =====
Les autres accords de septièmes sont dits « d'espèce ».
L'accord de septième mineure est l'accord de septième formé sur la fondamentale d'une gamme mineure ''naturelle''. Par exemple, l'accord de septième mineure de ''la'' est ''la''-''do''-''mi''-''sol''. Il est composé d'une tierce mineure, d'une quinte juste et d'une septième mineure (c'est un accord parfait mineur auquel on ajoute une septième mineure).
L'accord de septième majeure est l'accord de septième formé sur la fondamentale d'une gamme majeure. Par exemple, L'accord de septième majeure de ''do'' est ''do''-''mi''-''sol''-''si''. Il est composé d'une tierce majeure, d'une quinte juste et d'une septième majeure (c'est un accord parfait majeur auquel on ajoute une septième majeure).
==== Utilisation du chiffrage ====
Le chiffrage est utilisé de deux manières.
La première manière, c'est la notation de la basse continue. La basse continue est une technique d'improvisation utilisée dans le baroque pour l'accompagnement d'instruments solistes. Sur la partition, on indique en général la note de basse de l'accord et le chiffrage en chiffres arabes.
La seconde manière, c'est pour l'analyse d'une partition. Le fait de chiffrer les accords permet de mieux en comprendre la structure.
De manière générale, on peut retenir que :
* le chiffrage « 5 » indique un accord parfait, superposition d'une tierce (majeure ou mineure) et d'une quinte juste ;
* le chiffrage « 6 » indique le premier renversement d'un accord parfait ;
* le chiffrage « 6/4 » indique le second renversement d'un accord parfait ;
* chiffrage « 7/+ » indique un accord de septième de dominante ;
* le signe « + » indique en général que la note de l'intervalle est la sensible ;
* un intervalle barré désigne un intervalle diminué.
[[fichier:Accords gamme do majeur la mineur.svg|class=transparent| center | Principaux accords construits sur les gammes de ''do'' majeur et de ''la'' mineur harmonique.]]
=== Notation « jazz » ===
En jazz et de manière générale en musique rock et populaire, la base d'un accord est la triade composée d'une tierce (majeure ou mineure) et d'une quinte juste. Pour désigner un accord, on utilise la note fondamentale, éventuellement désigné par une lettre dans le système anglo-saxon (A pour ''la'' etc.), suivi d'une qualité (comme « m », « + »…).
Les renversements ne sont pas notés de manière particulière, ils sont notés comme les formes fondamentales.
Dans les deux tableaux suivants, la fondamentale est notée X (remplace le C pour un accord de ''do'', le D pour un accord de ''ré''…). La construction des accords est décrite par la suite.
[[Fichier:Arbre accords triades 5d5J5A.svg|vignette|upright=1.5|Formation des triades présentée sous forme d'arbre.]]
{| class="wikitable"
|+ Notation des principales triades
|-
|
! scope="col" | Tierce<br />mineure (3m)
! scope="col" | Tierce<br />majeure (3M)
|-
! scope="row" | Quinte diminuée (5d)
| X<sup>o</sup>, Xm<sup>♭5</sup>, X–<sup>♭5</sup> ||
|-
! scope="row" | Quinte juste (5J)
| Xm, X– || X
|-
! scope="row" | Quinte augmentée (5A)
| || X+, X<sup>♯5</sup>
|}
[[Fichier:Triades do.svg|class=transparent|center|Triades de do.]]
{| class="wikitable"
|+ Notation des principaux accords de septième
|-
| colspan="2" |
! scope="col" | Tierce<br />mineure (3m)
! scope="col" | Tierce<br />majeure (3M)
|-
! scope="row" rowspan="2" | Quinte<br />diminuée (5d)
! scope="row" | Septième diminuée (7d)
| X<sup>o7</sup> ||
|-
! scope="row" | Septième mineure (7m)
| Xm<sup>7(♭5)</sup>, X–<sup>7(♭5)</sup>, X<sup>Ø</sup> ||
|-
! scope="row" rowspan="3" | Quinte<br />juste (5J)
! scope="row" | Sixte majeure (6M)
| Xm<sup>6</sup> || X<sup>6</sup>
|-
! scope="row" | Septième mineure (7m)
| Xm<sup>7</sup>, X–<sup>7</sup> || X<sup>7</sup>
|-
! scope="row" | Septième majeure (7M)
| Xm<sup>maj7</sup>, X–<sup>maj7</sup>, Xm<sup>Δ</sup>, X–<sup>Δ</sup> || X<sup>maj7</sup>, X<sup>Δ</sup>
|-
! scope="row" rowspan="2" | Quinte<br />augmentée (5A)
! scope="row" | Septième mineure (7m)
| || X+<sup>7</sup>
|-
! scope="row" | Septième majeure (7M)
| || X+<sup>maj7</sup>
|}
[[Fichier:Arbre accords septieme.svg|class=transparent|center|Formation des accords de septième présentée sous forme d'arbre.]]
[[Fichier:Accords do septieme.svg|class=transparent|center|Accord de do septième.]]
On notera que l'intervalle de sixte majeure est l'enharmonique de celui de septième diminuée (6M = 7d).
[[File:Principaux accords do.svg|class=transparent|center|Principaux accords de do.]]
==== Triades ====
; Accords fondés sur une tierce majeure
* accord parfait majeur : pas de notation
*: p. ex. « ''do'' » ou « C » pour l'accord parfait de ''do'' majeur (''do'' - ''mi'' - ''sol'')
; Accords fondés sur une tierce mineure
* accord parfait mineur : « m », « min » ou « – »
*: « ''do'' m », « ''do'' – », « Cm », « C– »… pour l'accord parfait de ''do'' mineur (''do'' - ''mi''♭ - ''sol'')
==== Triades modifiées ====
; Accords fondés sur une tierce majeure
* accord augmenté (la quinte est augmentée) : aug, +, ♯5
*: « ''do'' aug », « ''do'' + », « ''do''<sup>♯5</sup> » « Caug », « C+ » ou « C<sup>♯5</sup> » pour l'accord de ''do'' augmenté (''do'' - ''mi'' - ''sol''♯)
: L'accord augmenté est un empilement de tierces majeures. Ainsi, un accord augmenté a deux notes communes avec deux autres accords augmentés : C+ (''do'' - ''mi'' - ''sol''♯) a deux notes communes avec A♭+ (''la''♭ - ''do'' - ''mi'') et avec E+ (''mi'' - ''sol''♯ - ''si''♯) ; et on remarque que ces trois accords sont en fait enharmoniques (avec les enharmonies ''la''♭ = ''sol''♯ et ''si''♯ = ''do''). En effet, l'octave comporte six tons (sous la forme de cinq tons et deux demi-tons), et une tierce majeure comporte deux tons, on arrive donc à l'octave en ajoutant une tierce majeure à la dernière note de l'accord.
; Accords fondés sur une tierce mineure
* accord diminué (la quinte est diminuée) : dim, o, ♭5
*: « ''do'' dim », « ''do''<sup>o</sup> », « ''do''<sup>♭5</sup> », « Cdim », « C<sup>o</sup> » ou « C<sup>♭5</sup> » pour l'accord de ''do'' diminuné (''do'' - ''mi''♭ - ''sol''♭)
: On remarque que la quinte diminuée est l'enharmonique de la quarte augmentée et est l'intervalle appelé « triton » (car composé de trois tons).
; Accords fondés sur une tierce majeure ou mineure
* accord suspendu de seconde : la tierce est remplacée par une seconde majeure : sus2
*: « ''do''<sup>sus2</sup> » ou « C<sup>sus2</sup> » pour l'accord de ''do'' majeur suspendu de seconde (''do''-''ré''-''sol'')
* accord suspendu de quarte : la tierce est remplacée par une quarte juste : sus4
*: « ''do''<sup>sus4</sup> » ou « C<sup>sus4</sup> » pour l'accord de ''do'' majeur suspendu de quarte (''do''-''fa''-''sol'')
==== Triades appauvries ====
; Accords fondés sur une tierce majeure ou mineure
* accord de puissance : la tierce est omise, l'accord n'est constitué que de la fondamentale et de la quinte juste : 5
*: « ''do''<sup>5</sup> », « C<sup>5</sup> » pour l'accord de puissance de ''do'' (''do'' - ''la'')
{{note|Très utilisé dans les musiques rock, hard rock et heavy metal, il est souvent joué renversé (''la'' - ''do'') ou bien avec l'ajout de l'octave (''do'' - ''la'' - ''do'').}}
==== Triades enrichies ====
; Accords fondés sur une tierce majeure
* accord de septième (la 7<sup>e</sup> est mineure) : 7
*: « ''do''<sup>7</sup> », « C<sup>7</sup> » pour l'accord de ''do'' septième, appelé « accord de septième de dominante de ''fa'' majeur » en musique classique (''do'' - ''mi'' - ''sol'' - ''si''♭)
* accord de septième majeure : Δ, 7M ou maj7
*: « ''do'' <sup>Δ</sup> », « ''do'' <sup>maj7</sup> », « C<sup>Δ</sup> », « C<sup>7M</sup> »… pour l'accord de ''do'' septième majeure (''do'' - ''mi'' - ''sol'' - ''si'')
; Accords fondés sur une tierce mineure
* accord de mineur septième (la tierce et la 7<sup>e</sup> sont mineures) : m7, min7 ou –7
*: « ''do'' m<sup>7</sup> », « ''do'' –<sup>7</sup> », « Cm<sup>7</sup> », « C–<sup>7</sup> »… pour l'accord de ''do'' mineur septième, appelé « accord de septième de dominante de ''fa'' mineur » en musique classique (''do'' - ''mi''♭ - ''sol'' - ''si''♭)
* accord mineure septième majeure : m7M, m7maj, mΔ, –7M, –7maj, –Δ
*: « ''do'' m<sup>7M</sup> », « ''do'' m<sup>maj7</sup> », « ''do'' –<sup>Δ</sup> », « Cm<sup>7M</sup> », « Cm<sup>maj7</sup> », « C–<sup>Δ</sup> »… pour l'accord de ''do'' mineur septième majeure (''do'' - ''mi''♭ - ''sol'' - ''si'')
* accord de septième diminué (la quinte et la septième sont diminuée) : dim 7 ou o7
*: « ''do'' dim<sup>7</sup> », « ''do''<sup>o7</sup> », « Cdim<sup>7</sup> » ou « C<sup>o7</sup> » pour l'accord de ''do'' septième diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si''♭)
* accord demi-diminué (seule la quinte est diminuée, la septième est mineure) : Ø ou –7(♭5)
*: « ''do''<sup>Ø</sup> », « ''do''<sup>7(♭5)</sup> », « C<sup>Ø</sup> » ou « C<sup>7♭5</sup> » pour l'accord de ''do'' demi-diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si'')
=== Construction pythagoricienne des accords ===
Nous avons vu au débuts que lorsque l'on joue deux notes en même temps, leurs vibrations se superposent. Certaines superpositions créent un phénomène de battement désagréable, c'est le cas des secondes.
Dans le cas d'une tierce majeure, les fréquences des notes quadruple et quintuple d'une même base : les fréquences s'écrivent 4׃<sub>0</sub> et 5׃<sub>0</sub>. Cette superposition de vibrations est agréable à l'oreille. Nous avons également vu que dans le cas d'une quinte juste, les fréquences sont le double et le triple d'une même base, ou encore le quadruple et sextuple si l'on considère la moitié de cette base.
Ainsi, dans un accord parfait majeur, les fréquences des fondamentales des notes sont dans un rapport 4, 5, 6. De même, dans le cas d'un accord parfait mineur, les proportions sont de 1/6, 1/5 et 1/4.
{{voir|[[../Caractéristiques_et_notation_des_sons_musicaux#Construction_pythagoricienne_et_gamme_de_sept_tons|Caractéristiques et notation des sons musicaux > Construction pythagoricienne et gamme de sept tons]]}}
=== Un peu de physique : interférences ===
Les sons sont des vibrations. Lorsque l'on émet deux sons ou plus simultanément, les vibrations se superposent, on parle en physique « d'interférences ».
Le modèle le plus simple pour décrire une vibration est la [[w:fr:Fonction sinus|fonction sinus]] : la pression de l'air P varie en fonction du temps ''t'' (en secondes, s), et l'on a pour un son « pur » :
: P(''t'') ≈ sin(2π⋅ƒ⋅''t'')
où ƒ est la fréquence (en hertz, Hz) du son.
Si l'on émet deux sons de fréquence respective ƒ<sub>1</sub> et ƒ<sub>2</sub>, alors la pression vaut :
: P(''t'') ≈ sin(2π⋅ƒ<sub>1</sub>⋅''t'') + sin(2π⋅ƒ<sub>2</sub>⋅''t'').
Nous avons ici une [[w:fr:Identité trigonométrique#Transformation_de_sommes_en_produits,_ou_antilinéarisation|identité trigonométrique]] dite « antilinéarisation » :
: <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{f_1 + f_2}{2}t \right ) \cdot \sin \left ( 2\pi \frac{f_1 - f_2}{2}t \right ).</math>
On peut étudier simplement deux situations simples.
[[Fichier:Battements interferentiels.png|vignette|Deux sons de fréquences proches créent des battements : la superposition d'une fréquence et d'une enveloppe.]]
La première, c'est quand les fréquences ƒ<sub>1</sub> et ƒ<sub>2</sub> sont très proches. Alors, la moyenne (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2 est très proche de ƒ<sub>1</sub> et ƒ<sub>2</sub> ; et la demie différence (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2 est très proche de zéro. On a donc une enveloppe de fréquence très faible, (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2, dans laquelle s'inscrit un son de fréquence moyenne, (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2. C'est cette enveloppe de fréquence très faible qui crée les battements, désagréables à l'oreille.
Sur l'image ci-contre, le premier trait rouge montre un instant où les vibrations sont opposées ; elles s'annulent, le son s'éteint. Le second trait rouge montre un instant où les vibrations sont en phase : elle s'ajoutent, le son est au plus fort.
{{clear}}
La seconde, c'est lorsque les deux fréquences sont des multiples entiers d'une même fréquence fondamentale ƒ<sub>0</sub> : ƒ<sub>1</sub> = ''n''<sub>1</sub>⋅ƒ<sub>0</sub> et ƒ<sub>0</sub> = ''n''<sub>0</sub>⋅ƒ<sub>0</sub>. On a alors :
: <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{n_1 + n_2}{2}f_0 \cdot t \right ) \cdot \sin \left ( 2\pi \frac{n_1 - n_2}{2}f_0 \cdot t \right ).</math>
On multiplie donc deux fonctions qui ont des fréquences multiples de ƒ<sub>0</sub>. La différence minimale entre ''n''<sub>1</sub> et ''n''<sub>2</sub> vaut 1 ; on a donc une enveloppe dont la fréquence est au minimum la moitié de ƒ<sub>0</sub>, c'est-à-dire un son une octave en dessous de ƒ<sub>0</sub>. Donc, cette enveloppe ne crée pas d'effet de battement, ou plutôt, le battement est trop rapide pour être perçu comme tel. Dans cette enveloppe, on a une fonction sinus dont la fréquence est également un multiple de ƒ<sub>0</sub> ; l'enveloppe et la fonction qui y est inscrite ont donc de nombreux « points communs », d'où l'effet harmonieux.
=== Le tonnetz ===
[[File:Speculum musicae.png|thumb|right|225px|Euler, ''De harmoniæ veris principiis'', 1774, p. 350.]]
En allemand, le terme ''Tonnetz'' (se prononce « tône-netz ») signifie « réseau tonal ». C'est une représentation graphique des notes qui a été imaginée par [[w:Leonhard Euler|Leonhard Euler]] en 1739.
Cette représentation graphique peut aider à la mémorisation de certains concepts de l'harmonie. Cependant, son application est très limitée : elle ne concerne que l'intonation juste d'une part, et que les accords parfait des tonalités majeures et mineures naturelles d'autre part. La représentation contenant les douze notes de la musique savante occidentale, on peut bien sûr représenter d'autres objets, comme les accords de septième ou les accords diminués, mais la représentation graphique est alors compliquée et perd son intérêt pédagogique.
On part d'une note, par exemple le ''do''. Si on progresse vers la droite, on monte d'une quinte juste, donc ''sol'' ; vers la gauche, on descend d'une quinte juste, donc ''fa''. Si on va vers le bas, on monte d'une tierce majeure, donc ''mi'' ; si on va vers le haut, on descend d'une tierce majeure, donc ''la''♭ ou ''sol''♯
fa — do — sol — ré
| | | |
la — mi — si — fa♯
| | | |
do♯ — sol♯ — ré♯ — si♭
La figure forme donc un filet, un réseau. On voit que ce réseau « boucle » : si on descend depuis le ''do''♯, on monte d'une tierce majeure, on obtient un ''mi''♯ qui est l'enharmonique du ''fa'' qui est en haut de la colonne. Si on va vers la droite à partir du ''ré'', on obtient le ''la'' qui est au début de la ligne suivante.
Si on ajoute des diagonales allant vers la droite et le haut « / », on met en évidence des tierces mineures : ''la'' - ''do'', ''mi'' - ''sol'', ''si'' - ''ré'', ''do''♯ - ''mi''…
fa — do — sol — ré
| / | / | / |
la — mi — si — fa♯
| / | / | / |
do♯ — sol♯ — ré♯ — si♭
Donc les liens représentent :
* | : tierce majeure ;
* — : quinte juste ;
* / : tierce mineure.
[[Fichier:Tonnetz carre accords fr.svg|thumb|Tonnetz avec les accords parfaits. Les notes sont en notation italienne et les accords en notation jazz.]]
On met ainsi en évidence des triangles dont un côté est une quinte juste, un côté une tierce majeure et un côté une tierce mineure ; c'est-à-dire que les notes aux sommets du triangle forment un accord parfait majeur (par exemple ''do'' - ''mi'' - ''sol'') :
<div style="font-family:courier; background-color:#fafafa">
fa — '''do — sol''' — ré<br />
| / '''| /''' | / |<br />
la — '''mi''' — si — fa♯<br />
| / | / | / |<br />
do♯ — sol♯ — ré♯ — si♭
</div>
ou un accord parfait mineur (''la'' - ''do'' - ''mi'').
<div style="font-family:courier; background-color:#fafafa">
fa — '''do''' — sol — ré<br />
| '''/ |''' / | / |<br />
'''la — mi''' — si — fa♯<br />
| / | / | / |<br />
do♯ — sol♯ — ré♯ — si♭
</div>
Un triangle représente donc un accord, et un sommet représente une note. Si on passe d'un triangle à un triangle voisin, alors on passe d'un accord à un autre accord, les deux accords ayant deux notes en commun. Ceci illustre la notion de « plus court chemin » en harmonie : si on passe d'un accord à un autre en gardant un côté commun, alors on a un mouvement conjoint sur une seule des trois voix.
Par rapport à l'harmonie fonctionnelle : les accords sont contigus à leur fonction, par exemple en ''do'' majeur :
* fonction de tonique ({{Times New Roman|I}}) : C, A– et E– sont contigus ;
* fonction de sous-dominante ({{Times New Roman|IV}}) : F et D– sont contigus ;
* fonction de dominante ({{Times New Roman|V}}) : G et B<sup>o</sup> sont contigus.
On notera que les triangles d'un schéma ''tonnetz'' ne représentent que des accords parfaits. Pour représenter un accord de quinte diminuée (''si'' - ''ré'' - ''fa'') ou les accords de septième, en particulier l'accord de septième de dominante, il faut étendre le ''tonnetz'' et l'on obtient des figures différentes. Par ailleurs, il est adapté à ce que l'on appelle « l'intonation juste », puisque tous les intervalles sont idéaux.
[[Fichier:Tonnetz carre accords etendu fr.svg|vignette|Tonnetz étendu.]]
[[Fichier:Tonnetz carre do majeur accords fr.svg|vignette|Tonnetz de la tonalité de ''do'' majeur. La représentation de l'accord de quinte diminuée sur ''si'' (B<sup>o</sup>) est une ligne et non un triangle.]]
[[Fichier:Tonnetz carre do mineur accords fr.svg|vignette|Tonnetz des tonalités de ''do'' mineur naturel (haut) et ''do'' mineur harmonique (bas).]]
Si l'on étend un peu le réseau :
ré♭ — la♭ — mi♭ — si♭ — fa
| / | / | / | / |
fa — do — sol — ré — la
| / | / | / | / |
la — mi — si — fa♯ — do♯
| / | / | / | / |
do♯ — sol♯ — ré♯ — la♯ — mi♯
| / | / | / | / |
mi♯ — do — sol — ré — la
on peut donc trouver des chemins permettant de représenter les accords de septième de dominante (par exemple en ''do'' majeur, G<sup>7</sup>)
fa
/
sol — ré
| /
si
et des accords de quinte diminuée (en ''do'' majeur : B<sup>o</sup>)
fa
/
ré
/
si
Une gamme majeure ou mineure naturelle peut se représenter par un trapèze rectangle : ''do'' majeur
fa — do — sol — ré
| /
la — mi — si
et ''do'' mineur
la♭ — mi♭ — si♭
/ |
fa — do — sol — ré
En revanche, la représentation d'une tonalité nécessite d'étendre le réseau afin de pouvoir faire figurer tous les accords, deux notes sont représentées deux fois. La représentation des tonalités mineures harmoniques prend une forme biscornue, ce qui nuit à l'intérêt pédagogique de la représentation.
[[Fichier:Neo-Riemannian Tonnetz.svg|vignette|upright=2|Tonnetz avec des triangles équilatéraux.]]
On peut réorganiser le schéma en décalant les lignes, afin d'avoir des triangles équilatéraux. Sur la figure ci-contre (en notation anglo-saxonne) :
* si on monte en allant vers la droite « / », on a une tierce mineure ;
* si on descend en allant vers la droite « \ », on a une tierce majeure ;
* les liens horizontaux « — » représentent toujours des quintes justes
* les triangles pointe en haut sont des accords parfaits mineurs ;
* les triangles pointe en bas sont des accords parfaits majeurs.
On a alors les accords de septième de dominante
F
/
G — D
\ /
B
et de quinte diminuée
F
/
D
/
B
les tonalités majeures
F — C — G — D
\ /
A — E — B
et les tonalités mineures naturelles
A♭ — E♭ — B♭
/ \
F — C — G — D
== Notes et références ==
{{références}}
== Voir aussi ==
=== Liens externes ===
{{wikipédia|Consonance (harmonie tonale)}}
{{wikipédia|Disposition de l'accord}}
{{wikisource|Petit Manuel d’harmonie}}
* {{lien web
| url = https://www.apprendrelesolfege.com/chiffrage-d-accords
| titre = Chiffrage d'accords (classique)
| site = Apprendrelesolfege.com
| consulté le = 2020-12-03
}}
* {{lien web
| url = https://www.coursd-harmonie.fr/introduction/introduction2_le_chiffrage_d_accords.php
| titre = Introduction II : Le chiffrage d'accords
| site = Cours d'harmonie.fr
| consulté le = 2021-12-14
}}
* {{lien web
| url=https://www.coursd-harmonie.fr/
| titre = Cours d'harmonie en ligne
| auteur = Jean-Baptiste Voinet
| site=coursd-harmonie.fr
| consulté le = 2021-12-20
}}
* {{lien web
| url=http://e-harmonie.e-monsite.com/
| titre = Cours d'harmonie classique en ligne
| auteur = Olivier Miquel
| site=e-harmonie
| consulté le = 2021-12-24
}}
* {{lien web
| url=https://fr.audiofanzine.com/theorie-musicale/editorial/dossiers/les-gammes-et-les-modes.html
| titre = Les bases de l’harmonie
| site = AudioFanzine
| date = 2013-07-23
| consulté le = 2024-01-12
}}
----
''[[../Mélodie|Mélodie]]'' < [[../|↑]] > ''[[../Représentation musicale|Représentation musicale]]''
[[Catégorie:Formation musicale (livre)|Harmonie]]
3yogs1p52ju9aybce2mhe7095bdlgef
745865
745864
2025-07-03T11:31:27Z
Cdang
1202
/* Principaux accords */ cohérence entre tableaux classique/jazz
745865
wikitext
text/x-wiki
{{Bases de solfège}}
<span style="font-size:25px;">6. Harmonie</span>
L'harmonie désigne les notes jouées en même temps, soit plusieurs instruments jouant chacun une note, soit un instrument jouant un accord (instrument dit polyphonique).
== Première approche ==
L'exemple le plus simple d'harmonie est sans doute la chanson en canon : c'est un chant polyphonique, c'est-à-dire à plusieurs voix, chaque voix chantant la même chose en décalé. Prenons par exemple ''Vent frais, vent du matin'' (la version originale est ''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609) :
[[Fichier:Vent frais vent du matin.svg|class=transparent|center|Partition de ''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]]
[[Fichier:Vent frais vent du matin.midi|vignette|''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]]
nous voyons que les voix se superposent de manière « harmonieuse ». Les notes de chaque voix se correspondent point par point (avec un retard), c'est donc un type d'harmonie polyphonique appelé « contrepoint ».
Considérons la première note de la mesure 6 pour chaque voix. Nous avons la superposition des notes ''ré''-''fa''-''la'' (du grave vers l'aigu) ; la superposition de notes jouées ou chantées ensembles s'appelle un accord. Cet accord ''ré''-''fa''-''la'' porte le nom « d'accord parfait de ''ré'' mineur » :
* « ''ré'' » car la note fondamentale est un ''ré'' ;
* « parfait » car il est l'association d'une tierce, ''ré''-''fa'', et d'une quinte juste, ''ré''-''la'' ;
* « mineur » car le premier intervalle, ''ré''-''fa'', est une tierce mineure.
Considérons maintenant un chant accompagné au piano. La piano peut jouer plusieurs notes en même temps, il peut jouer des accords.
[[Fichier:Au clair de le lune chant et piano.svg|class=transparent|center|Deux premières mesure d’Au clair de la lune.]]
[[Fichier:Au clair de le lune chant et piano.midi|vignette|Deux premières mesure d’Au clair de la lune.]]
L'accord, les notes à jouer simultanément, sont écrites « en colonne ». Lorsqu'on les énonce, on les lit de bas en haut mais le pianiste les joue en pressant les touches du clavier en même temps, de manière « plaquée ».
Le premier accord est composé des notes ''do''-''mi''-''sol'' ; il est appelé « accord parfait de ''do'' majeur » car la note fondamentale est ''do'', qu'il est l'association d'une tierce et d'une quinte juste et que le premier intervalle, ''do''-''mi'', est une tierce majeure.
== Consonance et dissonance ==
Les notions de consonance et de dissonance sont culturelles et changent selon l'époque. Nous pouvons néanmoins noter que :
* l'accord de seconde, et son renversement la septième, créent des battements, les notes « frottent », c'est un intervalle harmonique dissonant ; mais dans le cas de la septième, comme les notes sont éloignées, le frottement est moins perceptible ;
* les accords de tierce, quarte et quinte sonnent agréablement à l'oreille, ils sont consonants.
Dans la musique savante européenne, au début au du Moyen-Âge, seuls les accords de quarte et de quinte étaient considérés comme consonants, d'où leur qualification de « juste ». La tierce, et son renversement la sixte, étaient perçues comme dissonantes.
L'harmonie joue avec les consonances et les dissonances. Dans un premier temps, les harmonies dissonantes sont utilisées pour créer des tensions qui sont ensuite résolues, on utilise des successions « consonant-dissonant-consonant ». À force d'entendre des intervalles considérés comme dissonants, l'oreille s'habitue et certains finissent par être considérés comme consonants ; c'est ce qui est arrivé à la tierce et à la sixte à la fin du Moyen Âge avec le contrepoint.
Il faut ici aborder la notion d'harmonique des notes.
[[File:Harmoniques de do.svg|thumb|Les six premières harmoniques de ''do''.]]
Lorsque l'on joue une note, on entend d'autres notes plus aigües et plus faibles ; la note jouée est appelée la « fondamentale » et les notes plus aigües et plus faibles sont les « harmoniques ». C'est cette accumulation d'harmoniques qui donne la couleur au son, son timbre, qui fait qu'un piano ne sonne pas comme un violon. Par exemple, si l'on joue un ''do''<sup>1</sup><ref>Pour la notation des octaves, voir ''[[../Représentation_musicale#Désignation_des_octaves|Représentation musicale > Désignation des octaves]]''.</ref> (fondamentale), on entend le ''do''<sup>2</sup> (une octave plus aigu), puis un ''sol''<sup>2</sup>, puis encore un ''do''<sup>3</sup> plus aigu, puis un ''mi''<sup>3</sup>, puis encore un ''sol''<sup>3</sup>, puis un ''si''♭<sup>3</sup>…
Ainsi, puisque lorsque l'on joue un ''do'' on entend aussi un ''sol'' très léger, alors jouer un ''do'' et un ''sol'' simultanément n'est pas choquant. De même pour ''do'' et ''mi''. De là vient la notion de consonance.
Le statut du ''si''♭ est plus ambigu. Il fait partie des harmoniques qui sonnent naturellement, mais il forme une seconde descendante avec le ''do'', intervalle dissonant. Par ailleurs, on remarque que le ''si''♭ ne fait pas partie de la gamme de ''do'' majeur, contrairement au ''sol'' et au ''mi''.
Pour le jeu sur les dissonances, on peut écouter par exemple la ''Toccata'' en ''ré'' mineur, op. 11 de Sergueï Prokofiev (1912).
: {{lien web |url=https://www.youtube.com/watch?v=AVpnr8dI_50 |titre=Yuja Wang Prokofiev Toccata |site=YouTube |date=2019-02-26 |consulté le=2021-12-19}}
== Contrepoint ==
Dans le chant grégorien, la notion d'accord n'existe pas. L'harmonie provient de la superposition de plusieurs mélodies, notamment dans ce que l'on appelle le « contrepoint ».
Le terme provient du latin ''« punctum contra punctum »'', littéralement « point par point », et désigne le fait que les notes de chaque voix se correspondent.
L'exemple le plus connu de contrepoint est le canon, comme par exemple ''Frère Jacques'' : chaque note d'un couplet correspond à une note du couplet précédent.
Certains morceaux sont bâtis sur une écriture « en miroir » : l'ordre des notes est inversé entre les deux voix, ou bien les intervalles sont inversés (« mouvement contraire » : une tierce montante sur une voix correspond à une tierce descendante sur l'autre).
On peut également citer le « mouvement oblique » (une des voix, le bourdon, chante toujours la même note) et le mouvement parallèle (les deux voix chantent le même air mais transposé, l'une est plus aiguë que l'autre).
Nous reproduisons ci-dessous le début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.
[[Fichier:Haendel Sonate en trio re mineur debut canon.svg | vignette | center | upright=2 | Début du second ''Allergo'' de la sonate en trio en ''ré'' mineur de Haendel.]]
[[Fichier:Haendel Sonate en trio re mineur debut.midi | vignette | Début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.]]
Nous avons mis en évidence la construction en canon avec des encadrés de couleur : sur les quatre premières mesures, nous voyons trois thèmes repris alternativement par une voix et par l'autre. Ce type de procédé est très courant dans la musique baroque.
Les procédés du contrepoint s'appliquent également à la danse :
* unisson : les danseurs et danseuses font les mêmes gestes en même temps ;
* répétition : le fait de répéter une série de gestes, une « phrase dansante » ;
* canon : les gestes sont faits avec un décalage régulier d'un danseur ou d'une danseuse à l'autre ;
* cascade : forme de canon dans laquelle le décalage est très petit ;
* contraste : deux danseur·euses, ou deux groupes, ont des gestuelles très différentes ;
* accumulation : la gestuelle se complexifie par l'ajout d'éléments au fur et à mesure ; ou bien le nombre de danseur·euses augmente ;
* dialogue : les gestes de danseur·euses ou de groupes se répondent ;
* contre-point : la gestuelle d'un ou une danseuse se superpose à la gestuelle d'un groupe ;
* lâcher-rattraper : les danseurs et danseuses alternent danse à l'unisson et gestuelles indépendantes.
: {{lien web
| url=https://www.youtube.com/watch?v=wgblAOzedFc
| titre=Les procédés de composition en danse
| auteur= Doisneau Sport TV
| site=YouTube
| date=2020-03-16 | consulté le=2021-01-21
}}
{{...}}
== Les accords en général ==
Initialement, on a des chants polyphoniques, des voix qui chantent chacune une mélodie, les mélodies se mêlant. On remarque que certaines superpositions de notes sonnent de manière plus ou moins agréables, consonantes ou dissonantes. On en vient alors à associer ces notes, c'est-à-dire à considérer dès le départ la superposition de ces notes et non pas la rencontre de ces notes au gré des mélodies. Ces groupes de notes superposées forment les accords. En Europe, cette notion apparaît vers le {{pc|xiv}}<sup>e</sup> siècle avec notamment la ''[[wikipedia:fr:Messe de Notre Dame|Messe de Notre Dame]]'' de Guillaume de Machaut (vers 1360-1365). La notion « d'accord parfait » est consacrée par [[wikipedia:fr:Jean-Philippe Rameau|Jean-Philippe Rameau]] dans son ''Traité de l'harmonie réduite à ses principes naturels'', publié en 1722.
=== Qu'est-ce qu'un accord ? ===
Un accord est un ensemble d'au minimum trois notes jouées en même temps. « Jouées » signifie qu'il faut qu'à un moment donné, elles sonnent en même temps, mais le début ou la fin des notes peut être à des instants différents.
Considérons que l'on joue les notes ''do'', ''mi'' et ''sol'' en même temps. Cet accord s'appelle « accord de ''do'' majeur ». En musique classique, on lui adjoint l'adjectif « parfait » : « accord parfait de ''do'' majeur ».
Nous représentons ci-dessous trois manière de faire l'accord : avec trois instruments jouant chacun une note :
[[Fichier:Do majeur trois portees.svg|class=transparent|center|Accord de ''do'' majeur avec trois instruments différents.]]
Avec un seul instrument jouant simultanément les trois notes :
[[Fichier:Chord C.svg|class=transparent|center|Accord de ''do'' majeur joué par un seul instrument.]]
L'accord tel qu'il est joué habituellement par une guitare d'accompagnement :
[[Fichier:Do majeur guitare.svg|class=transparent|center|Accord de ''do'' majeur à la guitare.]]
Pour ce dernier, nous représentons le diagramme indiquant la position des doigts sur le manche au dessus de la portée et la tablature en dessous. Ici, c'est au total six notes qui sont jouées : ''mi'' grave, ''do'' médium, ''mi'' médium, ''sol'' médium, ''do'' aigu, ''mi'' aigu. Mais il s'agit bien des trois notes ''do'', ''mi'' et ''sol'' jouées à des octaves différentes. Nous remarquons également que la note de basse (la note la plus grave), ''mi'', est différente de la note fondamentale (celle qui donne le nom à l'accord), ''do'' ; l'accord est dit « renversé » (voir plus loin).
=== Comment joue-t-on un accord ? ===
Les notes ne sont pas forcément jouées en même temps ; elles peuvent être « égrainées », jouée successivement, ce que l'on appelle un arpège. La partition ci-dessous montre six manières différentes de jouer un accord de ''la'' mineur à la guitare, plaqué puis arpégé.
[[Fichier:La mineur differentes executions.svg|class=transparent|center|Différentes exécution de l'accord de do majeur à la guitare.]]
[[Fichier:La mineur differentes executions midi.midi|vignette|Différentes exécution de l'accord de la mineur à la guitare.]]
Vous pouvez écouter l'exécution de cette partition avec le lecteur ci-contre.
Seuls les instruments polyphoniques peuvent jouer les accords plaqués : instruments à clavier (clavecin, orgue, piano, accordéon), les instruments à plusieurs cordes pincées (harpe, guitare ; violon, alto, violoncelle et contrebasse joués en pizzicati). Les instruments à corde frottés de la famille du violon peuvent jouer des notes par deux à l'archet mais pas plus du fait de la forme bombée du chevalet ; cependant, un mouvement rapide permet de jouer les quatre cordes de manière très rapprochée. Les instruments à percussion de type xylophone ou le tympanon permettent de jouer jusqu'à quatre notes simultanément en tenant deux baguettes (mailloches, maillets) par main.
Tous les instruments peuvent jouer des arpèges même si, dans le cas des instruments monodiques, les notes ne continuent pas à sonner lorsque l'on passe à la note suivante.
L'arpège peut être joué par l'instrument de basse (basson, violoncelle, contrebasse, guitare basse, pédalier de l'orgue…), notamment dans le cas d'une basse continue ou d'une ''{{lang|en|walking bass}}'' (« basse marchante » : la basse joue des noires, donnant ainsi l'impression qu'elle marche).
En jazz, et spécifiquement au piano, on a recours au ''{{lang|en|voicing}}'' : on choisit la manière dont on organise les notes pour donner une couleur spécifique, ou bien pour créer une mélodie en enchaînant les accords. Il est fréquent de ne pas jouer toutes les notes : si on n'en garde que deux, ce sont la tierce et la septième, car ce sont celles qui caractérisent l'accord (selon que la tierce est mineure ou majeure, que la septième est majeure ou mineure), et la fondamentale est en général jouée par la contrebasse ou guitare basse.
{{clear}}
=== Classes d'accord ===
[[Fichier:Intervalles harmoniques accords classes.svg|vignette|upright=1.5|Intervalles harmoniques dans les accords classés de trois, quatre et cinq notes.]]
Un accord composé d'empilement de tierces est appelé « accord classé ». En musique tonale, c'est-à-dire la musique fondée sur les gammes majeures ou mineures (cas majoritaire en musique classique), on distingue trois classes d'accords :
* les accords de trois notes, ou triades, ou accords de quinte ;
* les accords de quatre notes, ou accords de septième ;
* les accords de cinq notes, ou accords de neuvième.
En empilant des tierces, si l'on part de la note fondamentale, on a donc de intervalles de tierce, quinte, septième et neuvième.
En musique tonale, les accords avec d'autres intervalles (hors renversement, voir ci-après), typiquement seconde, quarte ou sixte, sont considérés comme des transitions entre deux accords classés. Ils sont appelés, selon leur utilisation, « accords à retard » (en anglais : ''{{lang|en|suspended chord}}'', accord suspendu) ou « appoggiature » (note « appuyée », étrangère à l'harmonie). Voir aussi plus loin la notion de note étrangère.
=== Renversements d'accords ===
[[File:Accord do majeur renversements.svg|thumb|Accord parfait de do majeur et ses renversements.]]
[[Fichier:Progression dominante renverse parfait do majeur.svg|vignette|upright=0.6|Progression accord de dominante renversé → accord parfait en ''do'' majeur.]]
Un accord classé est donc un empilement de tierces. Si l'on change l'ordre des notes, on a toujours le même accord mais il est fait avec d'autres intervalles harmoniques. Par exemple, l'accord parfait de ''do'' majeur dans son état fondamental, c'est-à-dire non renversé, s'écrit ''do'' - ''mi'' - ''sol''. Sa note fondamentale, ''do'', est aussi se note de basse.
Si maintenant on prend le ''do'' de l'octave supérieure, l'accord devient ''mi - sol - do'' ; c'est l'empilement d'une tierce ''(mi - sol)'' et d'une quarte ''(sol - do)'', soit la superposition d'une tierce ''(mi - sol)'' et d'une sixième ''(mi - do)''. C'est le premier renversement de l'accord parfait de ''do'' majeur ; la fondamentale est toujours ''do'' mais la basse est ''mi''. Le second renversement est ''sol - do - mi''.
L'utilisation de renversement peut faciliter l'exécution de la progression d'accord. Par exemple, en tonalité ''do'' majeur, si l'on veut passer de l'accord de dominante ''sol - si - ré'' à l'accord parfait ''do - mi - sol'', alors on peut utiliser le second renversement de l'accord de dominante : ''ré - sol - si'' → ''do - mi - sol''. Ainsi, la basse descend juste d'un ton ''(ré → do)'' et sur un piano, la main reste globalement dans la même position.
Le renversement d'un accord permet également de respecter certaines règles de l'harmonie classique, notamment éviter que des voix se suivent strictement (« mouvement parallèle »), ce qui aurait un effet de platitude.
De manière générale, la notion de renversement permet deux choses :
* d'enrichir l'œuvre : pour créer une harmonie donnée (c'est-à-dire des sons sonnant bien ensemble), nous avons plus de souplesse, nous pouvons organiser ces notes comme nous le voulons selon les voix ;
* de simplifier l'analyse : quelle que soit la manière dont sont organisées les notes, cela nous ramène à un même accord.
{{citation bloc|Or il, y a plusieurs manières de jouer un accord, selon que l'on aborde par la première note qui le constitue, ''do mi sol'', la deuxième, ''mi sol do'', ou la troisième note, ''sol do mi''. Ce sont les renversements, [que Rameau] va classer en différentes combinaisons d'une seule matrice. Faisant cela, Rameau divise le nombre d'accords [de septième] par quatre. Il simplifie, il structure […].|{{ouvrage|prénom1=André |nom1=Manoukian |titre=Sur les routes de la musique |éditeur=Harper Collins |année=2021 |passage=54 |isbn=979-1-03391201-9}} }}
{{clear}}
[[File:Plusieurs realisation 1er renversement doM.svg|thumb|Plusieurs réalisation du premier renversement de l'accord de ''do'' majeur.]]
Notez que dans la notion de renversement, seule importe en fait la note de basse. Ainsi, les accords ''mi-sol-do'', ''mi-do-sol'', ''mi-do-mi-sol'', ''mi-sol-mi-do''… sont tous une déclinaison du premier renversement de ''do-mi-sol'' et ils seront abrégés de la même manière (''mi''<sup>6</sup> en musique classique ou C/E en musique populaire et jazz, voir plus bas).
{{clear}}
== Notation des accords de trois notes ==
Les accords de trois notes sont appelés « accords de quinte » en classique, et « triades » en jazz.
[[Fichier:Progression dominante renverse parfait do majeur chiffrage.svg|vignette|upright=0.7|Chiffrage du second renversement d'un accord de ''sol'' majeur et d'un accord de ''do'' majeur : notation en musique populaire et jazz (haut) et notation de basse chiffrée (bas).]]
Les accords sont construits de manière systématique. Nous pouvons donc les représenter de manière simplifiée. Cette notation simplifiée des accords est appelée « chiffrage ».
Reprenons la progression d'accords ci-dessus : « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » dans la tonalité de ''do'' majeur. On utilise en général trois notations différentes :
* en musique populaire, jazz, rock… un accord est désigné par sa note fondamentale ; ici donc, les accords sont notés « ''sol'' - ''do'' » ou, en notation anglo-saxonne, « G - C » ;<br /> comme le premier accord est renversé, on indique la note de basse après une barre, la progression d'accords est donc chiffrée '''« ''sol''/''ré'' - ''do'' »''' ou '''« G/D - C »''' ;<br /> il s'agit ici d'accords composés d'une tierce majeure et d'une quinte juste ; si les accords sont constitués d'intervalles différents, nous ajoutons un symbole après la note : « m » ou « – » si la tierce est mineure, « dim » ou « ° » si la quinte est diminuée ;
* en musique classique, on utilise la notation de « basse chiffrée » (utilisée notamment pour noter la basse continue en musique baroque) : on indique la note de basse sur la portée et on lui adjoint l'intervalle de la fondamentale à la note la plus haute (donc ici respectivement 6 et 5, puisque ''sol''-''si'' est une sixte et ''do''-''sol'' est une quinte), étant sous-entendu que l'on a des empilements de tierce en dessous ; mais dans le cas du premier accord, le premier intervalle n'est pas une tierce, mais une quarte ''(ré''-''sol)'', on note donc '''« ''ré'' <sup>6</sup><sub>4</sub> - ''do'' <sup>5</sup> »'''<ref>quand on ne dispose pas de la notation en supérieur (exposant) et inférieur (indice), on utilise parfois une notation sous forme de fraction : ''sol'' 6/4 et ''do'' 5/.</ref> ;
* lorsque l'on fait l'analyse d'un morceau, on s'attache à identifier la note fondamentale de l'accord (qui est différente de la basse dans le cas d'un renversement) ; on indique alors le degré de la fondamentale : '''« {{Times New Roman|V<sup>6</sup><sub>4</sub> - I<sup>5</sup>}} »'''.
La notation de basse chiffrée permet de construire l'accord à la volée :
* on joue la note indiquée (basse) ;
* s'il n'y a pas de 2 ni de 4, on lui ajoute la tierce ;
* on ajoute les intervalles indiqués par le chiffrage.
La notation de musique jazz oblige à connaître la composition des différents accords, mais une fois que ceux-ci sont acquis, il n'y a pas besoin de reconstruire l'accord.
La notation de basse chiffrée avec les chiffres romains n'est pas utilisée pour jouer, mais uniquement pour analyser ; Sur les partitions avec basse chiffrée, il y a simplement les chiffrages indiqués au-dessus de la partie de basse. Le chiffrage avec le degré en chiffres romains présente l'avantage d'être indépendant de la tonalité et donc de se concentrer sur la fonction de l'accord au sein de la tonalité. Par exemple, ci-dessous, nous pouvons parler de la progression d'accords « {{Times New Roman|V - I}} » de manière générale, cette notation étant valable quelle que soit la tonalité.
[[File:Progression dominante renverse parfait do majeur chiffrage basse continue.svg|thumb|Chiffrage en notation basse chiffrée de la progression d'accords « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » en do majeur.]]
{{note|En notation de base continue avec fondamentale en chiffres romains, la fondamentale est toujours indiquée ''sous'' la portée de la partie de basse. Les intervalles sont indiqués au-dessus de la portée de la partie de basse ; lorsque l'on fait une analyse, on peut ayssi les indiquer à côté du degré en chiffres romains, donc sous la portée de la basse.}}
{{note|En notation rock, le 5 en exposant indique un accord incomplet avec uniquement la fondamentale et la quinte, un accord sans tierce appelé « accord de puissance » ou ''{{lang|en|power chord}}''. Par exemple, C<sup>5</sup> est l'accord ''do-sol''.}}
{{clear}}
[[Fichier:Accords parfait do majeur basse chiffree fondamental et renverse.svg|vignette|upright=2.5|Chiffrage de l'accord parfait de ''do'' majeur en basse chiffrée, à l'état fondamental et ses renversements.]]
Concernant les accords parfaits en notation de basse chiffrée :
* un accord parfait à l'état fondamental est chiffré « <sup>5</sup> » ; on l'appelle « accord de quinte » ;
* le premier renversement est chiffré « <sup>6</sup> » (la tierce est implicite) ; on l'appelle « accord de sixte » ;
* le second renversement est noté « <sup>6</sup><sub>4</sub> » ; on l'appelle « accord de sixte et de quarte » (ou bien « de quarte et de sixte »).
Par exemple, pour l'accord parfait de ''do'' majeur :
* l'état fondamental ''do''-''mi''-''sol'' est noté ''do''<sup>5</sup> ;
* le premier renversement ''mi''-''sol''-''do'' est noté ''mi''<sup>6</sup> ;
* le second renversement ''sol''-''do''-''mi'' est noté ''sol''<sup>6</sup><sub>4</sub>.
Il y a une exception : l'accord construit sur la sensible (7{{e}} degré) contient une quinte diminuée et non une quinte juste. Le chiffrage est donc différent :
* l'état fondamental ''si''-''ré''-''fa'' est noté ''si''<sup><s>5</s></sup> (cinq barré), « accord de quinte diminuée » ;
* le premier renversement ''ré''-''fa''-''si'' est noté ''ré''<sup>+6</sup><sub>3</sub>, « accord de sixte sensible et tierce » ;
* le second renversement ''fa''-''si''-''ré'' est noté ''fa''<sup>6</sup><sub>+4</sub>, « accord de sixte et quarte sensible ».
Par ailleurs, on ne considère pas qu'il est fondé sur la sensible, mais sur la dominante ; on met donc des guillemets autour du degré, « “V” ». Donc selon l'état, le chiffrage est “V”<sup><s>5</s></sup>, “V”<sup>+6</sup><sub>3</sub> ou “V”<sup>6</sup><sub>+4</sub>.
En notation jazz, on ajoute « dim », « <sup>o</sup> » ou bien « <sup>♭5</sup> » au chiffrage, ici : B dim, B<sup>o</sup> ou B<sup>♭5</sup> pour l'état fondamental. Pour les renversements : B dim/D et B dim/F ; ou bien B<sup>o</sup>/D et B<sup>o</sup>/F ; ou bien B<sup>♭5</sup>/D et B<sup>♭5</sup>/F.
{{clear}}
[[Fichier:Accords basse chiffree basse do fondamental et renverses.svg|vignette|upright=2|Basse chiffrée : accords de quinte, de sixte et de sixte et de quarte ayant pour basse ''do''.]]
Et concernant les accords ayant pour basse ''do'' en tonalité de ''do'' majeur :
* l'accord ''do''<sup>5</sup> est un accord à l'état fondamental, c'est donc l'accord ''do''-''mi''-''sol'' (sa fondamentale est ''do'') ;
* l'accord ''do''<sup>6</sup> est le premier renversement d'un accord, c'est donc l'accord ''do''-''mi''-''la'' (sa fondamentale est ''la'') ;
* l'accord ''do''<sup>6</sup><sub>4</sub> est le second renversement d'un accord, c'est donc l'accord ''do''-''fa''-''la'' (sa fondamentale est ''fa'').
{{clear}}
== Notes étrangères ==
La musique européenne s'appuie essentiellement sur des accords parfaits, c'est-à-dire fondés sur une tierce majeure ou mineure, et une quinte juste. Il arrive fréquemment qu'un accord ne soit pas un accord parfait. Les notes qui font partie de l'accord parfait sont appelées « notes naturelles » et la note qui n'en fait pas partie est appelée « note étrangère ».
Il existe plusieurs types de notes étrangères :
* anticipation : la note étrangère est une note naturelle de l'accord suivant ;
* appogiature : note d'ornementation qui se résout par mouvement conjoint, c'est-à-dire qu'elle est suivie par une note située juste au-dessus ou en dessous (seconde ascendante ou descendante) qui est, elle, une note naturelle ;
* broderie : on part d'une note naturelle, on monte ou on descend d'une seconde, puis on revient sur la note naturelle ;
* double broderie : on part d'une note naturelle, on joue la note du dessus puis la note du dessous avant de revenir à la note naturelle ; ou bien on joue la note du dessous puis la note du dessus ;
* échappée : note étrangère n'appartenant à aucune des autres catégories ;
* note de passage : mouvement conjoint allant d'une note naturelle d'un accord à une note naturelle de l'accord suivant ;
* pédale : la note de basse reste la même pendant plusieurs accords successifs ;
* retard : la note étrangère est une note naturelle de l'accord précédent.
Les notes étrangères ne sont pas chiffrées.
[[File:Notes etrangeres accords.svg|center|Différents types de notes étrangères.]]
{{note|Les anglophones distinguent deux types de retard : la ''{{lang|en|suspension}}'' est résolue vers le haut (le mouvement est ascendant), le ''{{lang|en|retardation}}'' est résolu vers le bas (le mouvement est descendant).}}
== Principaux accords ==
Les trois principaux accords sont :
* l'accord parfait majeur : il est construit sur les degrés {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (médiante) et {{Times New Roman|V}} (dominante) d'une gamme majeure ; il est noté {{Times New Roman|I}}<sup>5</sup>, {{Times New Roman|IV}}<sup>5</sup>, {{Times New Roman|V}}<sup>5</sup> ;
* l'accord parfait mineur : il est construit sur les degrés {{Times New Roman|I}} (tonique) et {{Times New Roman|IV}} (sous-tonique) d'une gamme mineure harmonique ; il est également noté {{Times New Roman|I}}<sup>5</sup> et {{Times New Roman|IV}}<sup>5</sup>, les anglo-saxons le notent {{Times New Roman|i}}<sup>5</sup> et {{Times New Roman|iv}}<sup>5</sup> (la minuscule indiquant le caractère mineur) ;
* l'accord de septième de dominante : il est construit sur le degré {{Times New Roman|V}} (dominante) d'une gamme majeure ou mineure harmonique ; il est noté {{Times New Roman|V}}<sup>7</sup><sub>+</sub>.
On peut trouver ces trois accords sur d'autres degrés, et il existe d'autre types d'accords. Nous verrons cela plus loin.
{| class="wikitable"
|+ Constitution des principaux accords — Dénomination classique
|-
! scope="col" | Accord
! scope="col" | 1<sup>er</sup> intervalle
! scope="col" | 2<sup>e</sup> intervalle
! scope="col" | 3<sup>e</sup> intervalle
|-
! scope="row" | Accord parfait majeur
| tierce majeure (3M) || quinte juste (5J) || —
|-
! scope="row" | Accord parfait mineur
| tierce mineure (3m) || quinte juste (5J) || —
|-
! scope="row" | Accord de septième de dominante
| tierce majeure (3M) || quinte juste (5J) || septième mineure (7m)
|}
{| class="wikitable"
|+ Constitution des principaux accords — Dénomination jazz
|-
! scope="col" | Accord
! scope="col" | 1<sup>er</sup> intervalle
! scope="col" | 2<sup>e</sup> intervalle
! scope="col" | 3<sup>e</sup> intervalle
|-
! scope="row" | Triade majeure
| tierce majeure (3M) || quinte juste (5J) || —
|-
! scope="row" | Triade mineure
| tierce mineure (3m) || quinte juste (5J) || —
|-
! scope="row" | Accord de septième
| tierce majeure (3M) || quinte juste (5J) || septième mineure (7m)
|}
{| border="0"
|-
| [[Fichier:Accord do majeur arpege puis plaque.midi | Accord parfait de ''do'' majeur (C).]] || [[Fichier:Accord do mineur arpege puis plaque.midi | Accord parfait de ''do'' mineur (Cm).]] || [[Fichier:Accord do septieme arpege puis plaque.midi | Accord de septième de dominante de ''fa'' majeur (C<sup>7</sup>).]]
|-
| Accord parfait<br /> de ''do'' majeur (C). || Accord parfait<br /> de ''do'' mineur (Cm). || Accord de septième de dominante<br /> de ''fa'' majeur (C<sup>7</sup>).
|}
'''Rappel :'''
* la tierce mineure est composée d'un ton et demi (1 t ½) ;
* la tierce majeur est composée de deux tons (2 t) ;
* la quinte juste a la même altération que la fondamentale, sauf lorsque la fondamentale est ''si'' (la quinte juste est alors ''fa''♯) ;
* la septième mineure est le renversement de la seconde majeure (1 t).
[[File:Renversements accords pft fa maj basse chiffree.svg|thumb|Renversements de l'accord parfait de ''fa'' majeur, et la notation de basse chiffrée.]]
[[File:Renversements accord sept de dom fa maj basse chiffree.svg|thumb|Renversements de l'accord de septième de dominante de ''fa'' majeur, et la notation de basse chiffrée.]]
{| class="wikitable"
|+ Notation des principaux accords en musique classique
|-
! scope="col" | Accord
! scope="col" | État<br /> fondamental
! scope="col" | Premier<br /> renversement
! scope="col" | Deuxième<br /> renversement
! scope="col" | Troisième<br /> renversement
|-
! scope="row" | Accord parfait
| {{Times New Roman|I<sup>5</sup>}}<br/> acc. de quinte || {{Times New Roman|I<sup>6</sup>}}<br :> acc. de sixte || {{Times New Roman|I<sup>6</sup><sub>4</sub>}}<br /> acc. de quarte et de sixte || —
|-
! scope="row" | Accord de septième<br /> de dominante
| {{Times New Roman|V<sup>7</sup><sub>+</sub>}}<br /> acc.de septième de dominante || {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}<br />acc. de sixte et quinte diminuée || {{Times New Roman|V<sup>+6</sup>}}<br />acc. de sixte sensible || {{Times New Roman|V<sup>+4</sup>}}<br />acc. de quarte sensible<br />acc. de triton
|}
{| class="wikitable"
|+ Notation des principaux accords en jazz
|-
! scope="col" | Accord
! scope="col" | Chiffrage
! scope="col" | Renversements
|-
! scope="row" | Triade majeure
| X
| rowspan="3" | Les renversements se notent en mettant la basse après une barre de fraction, par exemple pour la triade de ''do'' majeur :
* état fondamental : C ;
* premier renversement : C/E ;
* second renversement : C/G.
|-
! scope="row" | Triade mineure
| Xm, X–
|-
! scope="row" | Septième
| X<sup>7</sup>
|}
{{clear}}
Dans le cas d'un accord de septième de dominante, le nom de l'accord change selon que l'on est en musique classique ou en jazz : en musique classique, on donne le nom de la tonalité alors qu'en jazz, on donne le nom de la fondamentale. Ainsi, l'accord appelé « septième de dominante de ''do'' majeur » en musique classique, est appelé « ''sol'' sept » (G<sup>7</sup>) en jazz : la dominante (degré {{Times New Roman|V}}, dominante) de la tonalité de ''do'' majeur est la note ''sol''.
Comment appelle-t-on en musique classique l'accord appelé « ''do'' sept » (C<sup>7</sup>) en jazz ? Les tonalités dont le ''do'' est la dominante sont les tonalités de ''fa'' majeur (''si''♭ à la clef) et de ''fa'' mineur harmonique (''si''♭, ''mi''♭, ''la''♭ et ''ré''♭ à la clef et ''mi''♮ accidentel). Il s'agit donc de l'accord de septième de dominante des tonalités de ''fa'' majeur et ''fa'' mineur harmonique.
{| class="wikitable"
|+ Accords fréquents pour quelques la tonalités majeures
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Accord parfait<br />{{Times New Roman|I<sup>5</sup>}}
! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}}
|-
|''Do'' majeur || || C<br />''do-mi-sol'' || G7<br />''sol-si-ré-fa''
|-
|''Sol'' majeur || ''fa''♯ || G<br />''sol-si-ré'' || D7<br />''ré-fa''♯''-la-do''
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || D<br />''ré-fa''♯''-la'' || A7<br />''la-do''♯''-mi-sol''
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A<br />''la-do''♯''-mi'' || E7<br />''mi-sol''♯''-si-ré''
|-
| ''Fa'' majeur || ''si''♭ || F<br />''fa-la-do'' || C7<br />''do-mi-sol-si''♭
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭<br />''si''♭''-ré-fa'' || F7<br />''fa-la-do-mi''♭
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭<br />''mi''♭''-sol-si''♭ || B♭7<br />''si''♭''-ré-fa-la''♭
|}
{| class="wikitable"
|+ Accords fréquents pour quelques la tonalités mineures harmoniques
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Accord parfait<br />{{Times New Roman|i<sup>5</sup>}}
! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}}
|-
|''La'' mineur<br />harmonique || || Am, A–<br />''la-do-mi'' || E7<br />''mi-sol''♯''-si-ré''
|-
|''Mi'' mineur<br />harmonique || ''fa''♯ || Em, E–<br />''mi-sol-si'' || B7<br />''si-ré''♯''-fa''♯''-la''
|-
|''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || Bm, B–<br />''si-ré-fa''♯ || F♯7<br />''fa''♯''la''♯''-do''♯''-mi''
|-
|''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || F♯m, F♯–<br />''fa''♯''-la-do''♯ || C♯7<br />''do''♯''-mi''♯''-sol''♯''-si''
|-
| ''Ré'' mineur<br />harmonique || ''si''♭ || Dm, D–<br />''ré-fa-la'' || A7<br />''la-do''♯''-mi-sol''
|-
| ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || Gm, G–<br />''sol-si''♭''-ré'' || D7<br />''ré-fa''♯''-la-do''
|-
| ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || Cm, C–<br />''do-mi''♭''-sol'' || G7<br />''sol-si''♮''-ré-fa''
|}
{{clear}}
== Accords sur les degrés d'une gamme ==
=== Harmonisation d'une gamme ===
[[Fichier:Accord trois notes gamme do majeur chiffre.svg|vignette|upright=1.2|Accords de trois note sur la gamme de ''do'' majeur, chiffrés.]]
On peut ainsi construire une triade par degré d'une gamme.
Pour une gamme majeure, les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|V<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|II<sup>5</sup>}}, {{Times New Roman|III<sup>5</sup>}}, {{Times New Roman|VI<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|ii<sup>5</sup>}}, {{Times New Roman|iii<sup>5</sup>}}, {{Times New Roman|vi<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}.
Les accords ont tous une quinte juste à l'exception de l'accord {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} qui a une quinte diminuée, raison pour laquelle le « 5 » est barré. C'est un accord dit « de quinte diminuée ». En jazz, l'accord diminué est noté « dim », « ° », « m<sup>♭5</sup> » ou « <sup>–♭5</sup> ».
Nous avons donc trois types d'accords (dans la notation jazz) : X (triade majeure), Xm (triade mineure) et X° (triade diminuée), la lettre X remplaçant le nom de la note fondamentale.
{{clear}}
[[Fichier:Accord trois notes gamme la mineur chiffre.svg|vignette|upright=1.2|Accords de trois notes sur une gamme de ''la'' mineur harmonique, chiffrés.]]
Pour une gamme mineure harmonique, les accords {{Times New Roman|III<sup>+5</sup>}}, {{Times New Roman|V<sup>♯</sup>}} et {{Times New Roman|VI<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|II<sup><s>5</s></sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|i<sup>5</sup>}}, {{Times New Roman|ii<sup><s>5</s></sup>}}, {{Times New Roman|iv<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}.
Les accords {{Times New Roman|ii<sup><s>5</s></sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} ont une quinte diminuée ; ce sont des accords dits « de quinte diminuée ». L'accord {{Times New Roman|III<sup>+5</sup>}} a une quinte augmentée ; le signe « plus » indique que la note de cinquième, le ''sol'' dièse, est la sensible. En jazz, l'accord est noté « aug » ou « <sup>+</sup> ». Les autres accords ont une quinte juste.
Aux trois accords générés par une gamme majeure (X, Xm et X°), nous voyons ici apparaître un quatrième type d'accord : la triade augmentée X<sup>+</sup>.
Nous remarquons que des gammes ont des accords communs. Par exemple, l'accord {{Times New Roman|ii<sup>5</sup>}} de ''do'' majeur est identique à l'accord {{Times New Roman|iv<sup>5</sup>}} de ''la'' mineur (il s'agit de l'accord Dm).
Quel que soit le mode, les accords construits sur la sensible (accord de quinte diminuée) sont rarement utilisés. S'ils le sont, c'est en tant qu'accord de septième de dominante sans fondamentale (voir ci-après). C'est la raison pour laquelle le chiffrage indique le degré {{Times New Roman|V}} entre guillemets, et non pas le degré {{Times New Roman|VII}} (mais pour des raisons de clarté, nous l'indiquons entre parenthèses au début).
En mode mineur, l'accord de quinte augmentée {{Times New Roman|iii<sup>+5</sup>}} est très peu utilisé (voir plus loin ''[[#Progression_d'accords|Progression d'accords]]''). C'est un accord considéré comme dissonant.
On voit que :
* un accord parfait majeur peut appartenir à cinq gammes différentes ;<br /> par exemple l'accord parfait de ''do'' majeur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> degré de la gamme de ''do'' majeur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' mineur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''mi'' mineur ;
* un accord parfait mineur peut appartenir à cinq gammes différentes ;<br />par exemple l'accord parfait de ''la'' mineur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> de la gamme de ''la'' mineur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''mi'' mineur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|III}}<sup>e</sup> degré de ''fa'' majeur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''do'' majeur ;
* un accord de quinte diminuée peut appartenir à trois gammes différentes ;<br />par exemple, l'accord de quinte diminuée de ''si'' est l'accord construit sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' majeur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''la'' mineur et sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' mineur ;
* un accord de quinte augmentée (à l'état fondamental) ne peut appartenir qu'à une seule gamme ;<br /> par exemple, l'accord de quinte augmentée de ''do'' est l'accord construit sur le {{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur.
{| class="wikitable"
|+ Notation jazz des triades
|-
| rowspan="2" colspan="2" |
! scope="col" colspan="2" | Tierce
|-
! scope="col" | 3m
! scope="col" | 3M
|-
! rowspan="3" | Quinte
! scope="row" | 5d
| Xᵒ, X<sub>m</sub><sup>(♭5)</sup> ||
|-
! scope="row" | 5J
| Xm, X– || X
|-
! scope="row" | 5A
| || X+, X<sup>(♯5)</sup>
|}
=== Harmonisation par des accords de septième ===
[[Fichier:Harmonisation gamme do majeur par septiemes chiffre.svg|vignette|upright=2|Harmonisation de la gamme de do majeur par des accords de septième.]]
Les accords de septième contiennent une dissonance et créent ainsi une tension. Ils sont très utilisés en jazz. Nous avons représenté ci-contre l'harmonisation de la gamme de ''do'' majeur.
La constitution des accords est la suivantes :
* tierce majeure (3M)
** quinte juste (5J)
*** septième mineure (7m) : sur le degré V, c'est l'accord de septième de dominante V<sup>7</sup><sub>+</sub>, noté X<sup>7</sup> (X pour G),
*** septième majeure (7M) : sur les degrés I et IV, appelés « accords de septième majeure » et notés aussi X<sup>maj7</sup> ou X<sup>Δ</sup> (X pour C ou F) ;
* tierce mineure (3m)
** quinte juste (5J)
*** septième mineure : sur les degrés ii, iii et vi, appelés « accords mineur septième » et notés Xm<sup>7</sup> ou X–<sup>7</sup> (X pour D, E ou A),
** quinte diminuée (5d)
*** septième mineure (7m) : sur le degré vii, appelé « accord demi-diminué » (puisque seule la quinte est diminuée) et noté X<sup>∅</sup> ou Xm<sup>7(♭5)</sup> ou X–<sup>7(♭5)</sup> (X pour B) ;<br /> en musique classique, on considère que c'est un accord de neuvième de dominante sans fondamentale.
Nous avons donc quatre types d'accords : X<sup>7</sup>, X<sup>maj7</sup>, Xm<sup>7</sup> et X<sup>∅</sup>
En jazz, on ajoute souvent la quarte à l'accord de sous-dominante IV (sur le ''fa'' dans une gamme de ''do'' majeur) ; il s'agit ici d'une quarte augmentée (''fa''-''si'') et l'accord est surnommé « accord lydien » mais cette dénomination est erronée (il s'agit d'une mauvaise interprétation de textes antiques). C'est un accord de onzième sans neuvième (la onzième étant l'octave de la quarte), il est noté X<sup>maj7(♯11)</sup> ou X<sup>Δ(♯11)</sup> (ici, F<sup>maj7(♯11)</sup>, ''fa''-''la''-''do''-''mi''-''si'' ou ''fa''-''la''-''si''-''do''-''mi'').
=== Modulation et emprunt ===
Un morceau peut comporter des changements de tonalité; appelés « modulation ». Il y a parfois un court passage dans une tonalité différente, typiquement sur une ou deux mesures, avant de retourner dans la tonalité d'origine : on parle d'emprunt. Lorsqu'il y a une modulation ou un emprunt, les degrés changent. Un même accord peut donc avoir une fonction dans une partie du morceau et une autre fonction ailleurs. L'utilisation d'accord différents, et en particulier d'accord utilisant des altérations accidentelles, indique clairement une modulation.
Nous avons vu précédemment que les modulations courantes sont :
* les modulations dans les tons voisins ;
* les modulations homonymes ;
* les marches harmoniques.
Une modulation entre une tonalité majeure et mineure change la couleur du passage,
* la modulation la plus « douce » est entre les tonalités relatives (par exemple''do'' majeur et ''la'' mineur) car ces tonalités utilisent quasiment les mêmes notes ;
* la modulation la plus « voyante » est la modulation homonyme (par exemple entre ''do'' majeur et ''do'' mineur).
Une modulation commence souvent sur l'accord de dominante de la nouvelle tonalité.
Pour analyser un œuvre, ou pour improviser sur une partie, il est important de reconnaître les modulations. La description de la successind es tonalités s'appelle le « parcours tonal ».
=== Exercices élémentaires ===
L'apprentissage des accords passe par quelques exercices élémentaires.
'''1. Lire un accord'''
Il s'agit de lecture de notes : des notes composant les accords sont écrites « empilées » sur une portée, il faut les lire en énonçant les notes de bas en haut.
'''2. Reconnaître la « couleur » d'un accord'''
On écoute une triade et il faut dire si c'est une triade majeure ou mineure. Puis, on complexifie l'exercice en ajoutant la septième.
'''3. Chiffrage un accord'''
Trouver le nom d'un accord à partir des notes qui le composent.
'''4. Réalisation d'un accord'''
Trouver les notes qui composent un accord à partir de son nom.
'''5. Dictée d'accords'''
On écoute une succession d'accords et il faut soit écrire les notes sur une portée, soit écrire les noms de accords.
[[File:Exercice constitution accord basse chiffree.svg|thumb|Exercice : constitution d'accord à partir de la basse chiffrée.]]
'''Exercices de basse chiffrée'''
''Réalisation d'un accord''
Sur la figure suivante, écrire les notes des accords correspondant à la basse chiffrée. Déterminer le degré de la fondamentale pour chaque accord en considérant que nous sommes dans la tonalité de ''sol'' majeur.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice constitution accord basse chiffree solution.svg|vignette|Solution.]]
# La note de basse est un ''do''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''mi'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''sol''.<br />Le chiffrage « <sup>5</sup> » indique que c'est un accord dans son état fondamental (l'écart entre deux notes consécutives ne dépasse pas la tierce), la fondamentale est donc la basse, ''do'', qui est le degré IV de la tonalité.
# La note de basse est un ''si''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''ré'', puis nous appliquons le chiffrage 6 et ajoutons la sixte, ''sol''.<br />Le chiffrage « <sup>6</sup> » indique que c'est un accord dans son premier renversement. En le remettant dans son état fondamental, nous obtenons ''sol-si-ré'', la fondamentale est donc la tonique, le degré I.
# La note de basse est un ''la''. Nous ajoutons la tierce (chiffre 3), ''do'', et la sixte (6), ''fa''♯. Nous vérifions que le ''fa''♯ est la sensible (signe +)<br />Nous voyons un « blanc » entre les notes ''do'' et ''fa''♯. En descendant le ''fa''♯ à l'octave inférieure, nous obtenons un empilement de tierces ''fa''♯-''la-do'', le fondamentale est donc ''fa''♯, le degré VII. Nous pouvons le voir comme le deuxième renversement de l'accord de septième de dominante, sans fondamentale.
# La note de basse est un ''fa''♯. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''la'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''do'' ; nous vérifions qu'il s'agit bien d'une quinte diminuée (le 5 est barré). Nous appliquons le chiffre 6 et ajoutons la sixte, ''ré''.<br />Nous voyons que les notes ''do'' et ''ré'' sont conjointes (intervalle de seconde). En descendant le ''ré'' à l'octave inférieure, nous obtenons un empilement de tierces ''ré-fa''♯-''la-do'', le fondamentale est donc ''ré'', le degré V. Nous constatons que l'accord chiffré est le premier renversement de l'accord de septième de dominante.
{{boîte déroulante/fin}}
{{clear}}
''Chiffrage d'accords''
[[Fichier:Exercice chiffrage accord basse chiffree.svg|vignette|Accords à chiffrer.]]
Chiffrer les accords ci-contre.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice chiffrage accord basse chiffree solution.svg|vignette|Solution.]]
# On relève les intervalles en partant de la basse : tierce majeure (3M) et quinte juste (5J). Le chiffrage complet est donc ''fa''<sup>5</sup><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''fa''<sup>5</sup>.<br /> On peut aussi reconnaître que c'est l'accord parfait sur la tonique de la tonalité de ''fa'' majeur dans son état fondamental, le chiffrage d'un accord parfait étant <sup>5</sup>.
# On relève les intervalles en partant de la basse : quarte juste (4J), sixte majeure (6M). Le chiffrage complet est donc ''fa''<sup>6</sup><sub>4</sub>.<br /> On peut aussi reconnaître que c'est le second renversement de l'accord ''mi-sol-si'', sur la tonique de la tonalité de ''mi'' mineur, le chiffrage du second renversement d'un accord parfait étant <sup>6</sup><sub>4</sub>.
# Les intervalles en partant de la basse sont : tierce mineure (3m), quinte diminuée (5d), sixte mineure (6m). Le chiffrage complet est donc ''mi''<sup>6</sup><small><s>5</s></small><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''mi''<sup>6</sup><sub><s>5</s></sub>.<br /> On reconnaît le premier renversement de l'accord ''do-mi-sol-si''♭, accord de septième de dominante de la tonalité de ''fa'' majeur.
# Les intervalles en partant de la basse sont : tierce mineure (3m), quinte juste (5J), septième mineure (7m). Le chiffrage complet est donc ''ré''<sup>7</sup><small>5</small><sub>3</sub> ; c'est typique d'un accord de septième de dominante, son chiffrage est donc ''ré''<sup>7</sup><sub>+</sub>.<br /> On reconnaît l'accord de septième de dominante de la tonalité de ''sol'' mineur dans son état fondamental.
{{boîte déroulante/fin}}
{{clear}}
[[File:Exercice constitution accord notation jazz.svg|thumb|Exercice : constitution d'un accord d'après son chiffrage en notation jazz.]]
'''Exercices de notation jazz'''
''Réalisation d'un accord''
Sur la figure suivante, écrire les notes des accords correspondant aux chiffrages.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice constitution accord notation jazz solution.svg|thumb|Solution.]]
# Il s'agit de la triade majeure de ''do'' dans son état fondamental. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''do-mi-sol''.
# Il s'agit de la triade majeure de ''sol''. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''sol-si-ré''. On renverse l'accord afin que la basse soit le ''si'', l'accord est donc ''si-ré-sol''.
# Il s'agit de l'accord demi-diminué de ''fa''♯. Les intervalles sont la tierce mineure (3m), la quinte diminuée (5d) et la septième mineure (7m). Les notes sont donc ''fa''♯-''la-do-mi''. Nous renversons l'accord afin que la basse soit le ''la'', l'accord est donc ''a-do-mi-fa''♯.
# Il s'agit de l'accord de septième de ''ré''. Les intervalles sont donc la tierce majeure (3M), la quinte juste (5J) et la septième mineure (7m). Les notes sont ''ré-fa''♯''-la-do''. Nous renversons l'accord afin que la basse soit le ''fa''♯, l'accord est donc ''fa''♯''-la-do-ré''.
{{boîte déroulante/fin}}
{{clear}}
''Chiffrage d'accords''
[[File:Exercice chiffrage accord notation jazz.svg|thumb|Accords à chiffrer.]]
Chiffrer les accords ci-contre.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice chiffrage accord notation jazz solution.svg|thumb|Solution.]]
# Les notes sont toutes sur des interlignes consécutifs, c'est donc un empilement de tierces ; l'accord est dans son état fondamental. Les intervalles sont une tierce majeure (''fa-la'' : 3M) et une quinte juste (''fa-do'' : 5J), c'est donc la triade majeure de ''fa''. Le chiffrage est F.
# Il y a un blanc dans l'empilement des notes, c'est donc un accord renversé. En permutant les notes pour n'avoir que des tierces, on trouve l'accord ''mi-sol-si''. Les intervalles sont une tierce mineure (''mi-sol'' : 3m) et une quinte juste (''mi-si'' : 5J), c'est donc la triade mineure de ''mi'' avec un ''si'' à la basse. Le chiffrage est Em/B ou E–/B.
# Il y deux notes conjointes, c'est donc un renversement. L'état fondamental de cet accord est ''do-mi-sol-si''♭. Les intervalles sont une tierce majeure (''do-mi'' : 3M), une quinte juste (''do-sol'' : 5J) et une septième mineure (''do-si''♭ : 7m). C'est donc l'accord de ''do'' septième avec un ''mi'' à la basse, chiffré C<sup>7</sup>/E.
# Les notes sont toutes sur des interlignes consécutifs, l'accord est dans son état fondamental. Les intervalles sont la tierce mineure (''ré-fa'' : 3m), une quinte juste (''ré-la'' : 5J) et une septième mineure (''ré-do'' : 7m). C'est donc l'accord de ''ré'' mineur septième, chiffré Dm<sup>7</sup> ou D–<sup>7</sup>.
{{boîte déroulante/fin}}
{{clear}}
== Harmonie fonctionnelle ==
Le choix des accords et de leur succession — la progression des accords — est un élément important d'un morceau, de sa composition. Le compositeur ou la compositrice a bien sûr une liberté totale, mais pour faire des choix, il faut comprendre les conséquences de ces choix, et donc ici, les effets produits par les accords et leur progression.
Une des manières d'aborder le sujet est l'harmonie fonctionnelle.
=== Les trois fonctions des accords ===
En harmonie tonale, on considère que les accords ont une fonction. Il existe trois fonctions :
* la fonction de tonique, {{Times New Roman|I}} ;
* la fonction de sous-dominante, {{Times New Roman|IV}} ;
* la fonction de dominante, {{Times New Roman|V}}.
L'accord de tonique, {{Times New Roman|I}}, est l'accord « stable » de la tonalité par excellence. Il conclut en général les morceaux, et ouvre souvent les morceaux ; il revient fréquemment au cours du morceau.
L'accord de dominante, {{Times New Roman|V}}, est un accord qui introduit une instabilité, une tension. En particulier, il contient la sensible (degré {{Times New Roman|VI}}), qui est une note « aspirée » vers la tonique. Cette tension, qui peut être renforcée par l'utilisation d'un accord de septième, est fréquemment résolue par un passage vers l'accord de tonique. Nous avons donc deux mouvements typiques : {{Times New Roman|I}} → {{Times New Roman|V}} (création d'une tension, d'une attente) et {{Times New Roman|V}} → {{Times New Roman|I}} (résolution d'une tension). Les accords de tonique et de dominante ont le cinquième degré en commun, cette note sert donc de pivot entre les deux accords.
L'accord de sous-dominante, {{Times New Roman|IV}}, est un accord qui introduit lui aussi une tension, mais moins grande : il ne contient pas la sensible. Notons que s'il est une quarte au-dessus de la tonique, il est aussi une quinte en dessous d'elle ; il est symétrique de l'accord de dominante. Il a donc un rôle similaire à l'accord de dominante, mais atténué. L'accord de sous-dominante aspire soit vers l'accord de dominante, très proche, et l'on a alors une augmentation de la tension ; soit vers l'accord de tonique, un retour vers la stabilité (il a alors un rôle semblable à la dominante). Du fait de ces deux bifurcations possibles — augmentation de la tension ({{Times New Roman|IV}} → {{Times New Roman|V}}) ou retour à la stabilité ({{Times New Roman|IV}} → {{Times New Roman|I}}) —, l'utilisation de l'accord de sous-dominante introduit un certain flottement : si l'on peut facilement prédire l'accord qui suit un accord de dominante, on ne peut pas prédire ce qui suit un accord de sous-dominante.
Notons que la composition ne consiste pas à suivre ces règles de manière stricte, ce qui conduirait à des morceaux stéréotypés et plats. Le plaisir d'écoute joue sur une alternance entre satisfaction d'une attente (respect des règles) et surprise (rompre les règles).
=== Accords remplissant ces fonctions ===
Les accords sur les autres degrés peuvent se ramener à une de ces trois fonctions :
* {{Times New Roman|II}} : fonction de sous-dominante {{Times New Roman|IV}} ;
* {{Times New Roman|III}} (très peu utilisé en mode mineur en raison de sa dissonance) et {{Times New Roman|VI}} : fonction de tonique {{Times New Roman|I}} ;
* {{Times New Roman|VII}} : fonction de dominante {{Times New Roman|V}}.
En effet, les accords étant des empilements de tierces, des accords situés à une tierce l'un de l'autre — {{Times New Roman|I}} ↔ {{Times New Roman|III}}, {{Times New Roman|II}} ↔ {{Times New Roman|IV}}, {{Times New Roman|V}} ↔ {{Times New Roman|VII}}, {{Times New Roman|VI}} ↔ {{Times New Roman|VIII}} ( = {{Times New Roman|I}}) — ont deux notes en commun. On retrouve le fait que l'accord sur le degré {{Times New Roman|VII}} est considéré comme un accord de dominante sans tonique. En mode mineur, l'accord sur le degré {{Times New Roman|III}} est évité, il n'a donc pas de fonction.
{|class="wikitable"
|+ Fonction des accords
|-
! scope="col" | Fondamentale
! scope="col" | Fonction
|-
| {{Times New Roman|I}} || tonique
|-
| {{Times New Roman|II}} || sous-dominante faible
|-
| {{Times New Roman|III}} || tonique faible
|-
| {{Times New Roman|IV}} || sous-dominante
|-
| {{Times New Roman|V}} || dominante
|-
| {{Times New Roman|VI}} || tonique faible
|-
| {{Times New Roman|VII}} || dominante faible
|}
Par exemple en ''do'' majeur :
* fonction de tonique : '''''do''<sup>5</sup> (C)''', ''mi''<sup>5</sup> (E–), ''la''<sup>5</sup> (A–) ;
* fonction de sous-dominante : '''''fa''<sup>5</sup> (F)''', ''ré''<sup>5</sup> (D–) ;
* fonction de dominante : '''''sol''<sup>5</sup> (G)''' ou ''sol''<sup>7</sup><sub>+</sub> (G<sup>7</sup>), ''si''<sup> <s>5</s></sup> (B<sup>o</sup>).
En ''la'' mineur harmonique :
* fonction de tonique : '''''la''<sup>5</sup> (A–)''', ''fa''<sup>5</sup> (F) [, rarement : ''do''<sup>+5</sup> (C<sup>+</sup>)] ;
* fonction de sous-dominante : '''''ré''<sup>5</sup> (D–)''', ''si''<sup> <s>5</s></sup> (B<sup>o</sup>) ;
* fonction de dominante : '''''mi''<sup>5</sup> (E)''' ou ''mi''<sup>7</sup><sub>+</sub> (E<sup>7</sup>), ''sol''♯<sup> <s>5</s></sup> (G♯<sup>o</sup>).
Le fait d'utiliser des accords différents pour remplir une fonction permet d'enrichir l'harmonie, et de jouer sur l'équilibre entre satisfaction d'une attente (on respecte les règles sur les fonctions) et surprise (mais on n'utilise pas l'accord attendu).
=== Les dominantes secondaires ===
On utilise aussi des accords de septième dominante se fondant sur un autre degré que la dominante de la gamme ; on parle de « dominante secondaire ». Typiquement, avant un accord de septième de dominante, on utilise parfois un accord de dominante de dominante, dont le degré est alors noté « {{Times New Roman|V}} de {{Times New Roman|V}} » ou « {{Times New Roman|V}}/{{Times New Roman|V}} » ; la fondamentale est de l'accord est alors situé cinq degrés au-dessus de la dominante ({{Times New Roman|V}}), c'est donc le degré {{Times New Roman|IX}}, c'est-à-dire le degré {{Times New Roman|II}} de la tonalité en cours). Ou encore, on utilise un accord de dominante du degré {{Times New Roman|IV}} (« {{Times New Roman|V}} de {{Times New Roman|IV}} », la fondamentale est alors le degré {{Times New Roman|I}}) avant un accord sur le degré {{Times New Roman|IV}} lui-même.
Par exemple, en tonalité de ''do'' majeur, on peut trouver un accord ''ré - fa''♯'' - la - do'' (chiffré {{Times New Roman|V}} de {{Times New Roman|V}}<sup>7</sup><sub>+</sub>), avant un accord ''sol - si - ré - fa'' ({{Times New Roman|V}}<sup>7</sup><sub>+</sub>). L'accord ''ré - fa''♯'' - la - do'' est l'accord de septième de dominante des tonalités de ''sol''. Dans la même tonalité, on pourra utiliser un accord ''do - mi - sol - si''♭ ({{Times New Roman|V}} de {{Times New Roman|IV}}<sup>7</sup><sub>+</sub>) avant un accord ''fa - la - do'' ({{Times New Roman|IV}}<sup>5</sup>). Le recours à une dominante secondaire peut atténuer une transition, par exemple avec un enchaînement ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> → ''fa''<sup>5</sup> (C → C<sup>7</sup> → F) qui correspond à un enchaînement {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}} : le passage ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> (C → C<sup>7</sup>) se fait en ajoutant une note (le ''si''♭) et rend naturel le passage ''do'' → ''fa''.
Sur les sept degré de la gamme, on ne considère en général que cinq dominantes secondaires : en effet, la dominante du degré {{Times New Roman|I}} est la dominante « naturelle, primaire » de la tonalité (et n'est donc pas secondaire) ; et utiliser la dominante de {{Times New Roman|VII}} consisterait à considérer l'accord de {{Times New Roman|VII}} comme un accord propre, on évite donc les « {{Times New Roman|V}} de “{{Times New Roman|V}}” » (mais les « “{{Times New Roman|V}}” de {{Times New Roman|V}} » sont tout à fait « acceptables »).
=== Enchaînements classiques ===
Nous avons donc vu que l'on trouve fréquemment les enchaînements suivants :
* pour créer une instabilité :
** {{Times New Roman|I}} → {{Times New Roman|V}},
** {{Times New Roman|I}} → {{Times New Roman|IV}} (instabilité moins forte mais incertitude sur le sens d'évolution) ;
* pour maintenir l'instabilité :
** {{Times New Roman|IV}} → {{Times New Roman|V}} ;
* pour résoudre l'instabilité :
** {{Times New Roman|IV}} → {{Times New Roman|I}},
** {{Times New Roman|V}} → {{Times New Roman|I}}, cas particuliers (voir plus bas) :
*** {{Times New Roman|V}}<sup>+4</sup> → {{Times New Roman|I}}<sup>6</sup>,
*** {{Times New Roman|I}}<sup>6</sup><sub>4</sub> → {{Times New Roman|V}}<sup>7</sup><sub>+</sub> → {{Times New Roman|I}}<sup>5</sup>.
Les degrés indiqués ci-dessus sont les fonctions ; on peut donc utiliser les substitutions suivantes :
* {{Times New Roman|I}} par {{Times New Roman|VI}} et, en tonalité majeure, {{Times New Roman|III}} ;
* {{Times New Roman|IV}} par {{Times New Roman|II}} ;
* {{Times New Roman|V}} par {{Times New Roman|VII}}.
Pour enrichir l'harmonie, on peut utiliser les dominantes secondaires, en particulier :
* {{Times New Roman|V}} de {{Times New Roman|V}} ({{Times New Roman|II}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|V}},
* {{Times New Roman|V}} de {{Times New Roman|IV}} ({{Times New Roman|I}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|IV}}.
On peut enchaîner les enchaînements, par exemple {{Times New Roman|I}} → {{Times New Roman|IV}} → {{Times New Roman|V}}, ou encore {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}}… En jazz, on utilise très fréquemment l'enchaînement {{Times New Roman|II}} → {{Times New Roman|V}} → {{Times New Roman|I}} (deux-cinq-un).
On peut bien sûr avoir d'autres enchaînements, mais ces règles permettent d'analyser un grand nombre de morceaux, et donnent des clefs utiles pour la composition. Nous voyons ci-après un certain nombre d'enchaînements courants dans différents styles
== Exercice ==
Un hautboïste travaille la sonate en ''do'' mineur S. 277 de Heinichen. Sur le deuxième mouvement ''Allegro'', il a du mal à travailler un passage en raison des altérations accidentelles. Sur la suggestion de sa professeure, il décide d'analyser la progression d'accords sous-jacente afin que les altérations deviennent logiques. Il s'agit d'un duo hautbois et basson pour lequel les accords ne sont pas chiffrés, le basson étant ici un instrument soliste et non pas un élément de la basse continue.
Sur l'extrait suivant, déterminez les basses et la qualité (chiffrage) des accords sous-jacents. Commentez.
[[Fichier:Sonate hautbois basson heinichen 2e mvt mes49.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen.]]
{{note|L'œuvre est en ''do'' mineur et devrait donc avoir trois bémols à la clef, or ici il n'y en a que deux. En effet, le ''la'' pouvant être bécarre en mode mineur mélodique ascendant, le compositeur a préféré le noter explicitement en altération accidentelle lorsque l'on est en mode mélodique naturel, harmonique ou mélodique descendant. C'est un procédé assez courant à l'époque baroque.}}
{{boîte déroulante/début|titre=Solution}}
Une des difficultés ici est que les arpèges joués par les instruments sont agrémentés de notes de passage.
Les notes de la basse (du basson) sont différentes entre le premier et le deuxième temps de chaque mesure et ne peuvent pas appartenir au même accord. On a donc un accord par temps.
Sur le premier temps de chaque mesure, le basson joue une octave. La note concernée est donc la basse de chaque accord. Pour savoir s'il s'agit d'un accord à l'état fondamental ou d'un renversement, on regarde ce que joue le hautbois : dans un mouvement conjoint (succession d'intervalles de secondes), il est difficile de distinguer les notes de l'arpège des notes de passage, mais
: les notes des grands intervalles font partie de l'accord.
Ainsi, sur le premier temps de la première mesure (la basse est un ''mi''♭), on a une sixte descendante ''sol''-''si''♭ et, à la fin du temps, une tierce descendante ''sol''-''mi''♭. L'accord est donc ''mi''♭-''sol''-''si''♭, c'est un accord de quinte (accord parfait à l'état fondamental). À la fin du premier temps, le basson joue un ''do'', c'est donc une note étrangère.
Sur le second temps de la première mesure, le basson joue une tierce ascendante ''fa''-''la''♭, la première note est la basse de l'accord et la seconde une des notes de l'accord. Le hautbois commence par une sixte descendante ''la''♭-''do'', l'accord est donc ''fa''-''la''♭-''do'', un accord de quinte (accord parfait à l'état fondamental). Le ''do'' du basson la fin du premier temps est donc une anticipation.
Les autres notes étrangères de la première mesure sont des notes de passage.
Mais il faut faire attention : en suivant ce principe, sur les premiers temps des deuxième et troisième mesure, nous aurions des accords de septième d'espèce (puisque la septième est majeure). Or, on ne trouve pas, ou alors exceptionnellement, d'accord de septième d'espèce dans le baroque, mais quasi exclusivement des accords de septième de dominante. Donc au début de la deuxième mesure, le ''la''♮ est une appoggiature du ''si''♭, l'accord est donc ''si''♭-''ré''-''fa'', un asscord de quinte. De même, au début de la troisième mesure, le ''sol'' est une appoggiature du ''la''♭.
Il faut donc se méfier d'une analyse purement « mathématique ». Il faut s'attacher à ressentir la musique, et à connaître les styles, pour faire une analyse pertinente.
Ci-dessous, nous avons grisé les notes étrangères.
[[Fichier:Sonate hautbois basson heinichen 2e mvt mes49 analyse.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen. Analyse de la progression harmonique.]]
Le chiffrage jazz équivalent est :
: | E♭ F– | B♭<sup>Δ</sup> E♭ | A♭<sup>Δ</sup> D– | G …
Nous remarquons une progression assez régulière :
: ''mi''♭ ↗[2<sup>de</sup>] ''fa'' | ↘[5<sup>te</sup>] ''si''♭ ↗[4<sup>te</sup>] ''mi''♭ | ↘[5<sup>te</sup>] ''la''♭ ↗[4<sup>te</sup>] ''ré'' | ↘[5<sup>te</sup>] ''sol''
Le ''mi''♭ est le degré {{Times New Roman|III}} de la tonalité principale (''do'' mineur), c'est donc une tonique faible ; il « joue le même rôle » qu'un ''do''. S'il y avait eu un accord de ''do'' au début de l'extrait, on aurait eu une progression parfaitement régulière ↗[4<sup>te</sup>] ↘[5<sup>te</sup>].
Nous avons les modulations suivantes :
* mesure 49 : ''do'' mineur naturel (le ''si''♭ n'est pas une sensible) avec un accord sur “{{Times New Roman|I}}” (tonique faible, {{Times New Roman|III}}, pour la première analyse, ou bien tonique forte, {{Times New Roman|I}}, pour la seconde) suivi d'un accord sur {{Times New Roman|IV}} ;
* mesure 50 : ''si''♭ majeur avec un accord sur {{Times New Roman|I}} suivi d'un accord sur {{Times New Roman|IV}} ;
* mesure 51 : ''la''♭ majeur avec un accord sur {{Times New Roman|I}}, et emprunt à ''do'' majeur avec un accord sur {{Times New Roman|II}} ({{Times New Roman|IV}} faible).
On a donc une marche harmonique {{Times New Roman|I}} → {{Times New Roman|IV}} qui descend d'une seconde majeure (un ton) à chaque mesure (''do'' → ''si''♭ → ''la''♭), avec une exception sur la dernière mesure (modulation en cours de mesure et descente d'une seconde mineure au lieu de majeure).
Ce passage est donc construit sur une régularité, une règle qui crée un effet d'attente — enchaînement {{Times New Roman|I}}<sup>5</sup> → {{Times New Roman|IV}}<sup>5</sup> avec une marche harmonique d'une seconde majeure descendante —, et des « surprises », des exceptions au début — ce n'est pas un accord {{Times New Roman|I}}<sup>5</sup> mais un accord {{Times New Roman|III}}<sup>5</sup> — et à la fin — modulation en milieu de mesure et dernière descente d'une seconde mineure (½t ''la''♭ → ''sol'').
L'extrait ne permet pas de le deviner, mais la mesure 52 est un retour en ''do'' mineur, avec donc une modulation sur la dominante (accord de ''sol''<sup>7</sup><sub>+</sub>, G<sup>7</sup>).
{{boîte déroulante/fin}}
== Progression d'accords ==
Comme pour la mélodie, la succession des accords dans un morceau, la progression d'accords, suit des règles. Et comme pour la mélodie, les règles diffèrent d'un style musical à l'autre et la créativité consiste à parfois ne pas suivre ces règles. Et comme pour la mélodie, on part d'un ensemble de notes organisé, d'une gamme caractéristique d'une tonalité, d'un mode.
Les accords les plus utilisés pour une tonalité donnée sont les accords dont la fondamentale sont les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la tonalité, en particulier la triade {{Times New Roman|I}}, appelée « accord parfait » ou « accord de tonique », et l'accord de septième {{Times New Roman|V}}, appelé « septième de dominante ».
Le fait d'avoir une progression d'accords qui se répète permet de structurer un morceau. Pour les morceaux courts, il participe au plaisir de l'écoute et facilite la mémorisation (par exemple le découpage couplet-refrain d'une chanson). Sur les morceaux longs, une trop grande régularité peut introduire de la lassitude, les longs morceaux sont souvent découpés en parties présentant chacune une progression régulière. Le fait d'avoir une progression régulière permet la pratique de l'improvisation : cadence en musique classique, solo en jazz et blues.
; Note
: Le terme « cadence » désigne plusieurs choses différentes, et notamment en harmonie :
:* une partie improvisée dans un opéra ou un concerto, sens utilisé ci-dessus ;
:* une progression d'accords pour ponctuer un morceau et en particulier pour le conclure, sens utilisé dans la section suivante.
=== Accords peu utilisés ===
En mode mineur, l'accord de quinte augmentée {{Times New Roman|III<sup>+5</sup>}} est très peu utilisé. C'est un accord dissonant ; il intervient en général comme appogiature de l'accord de tonique (par exemple en ''la'' mineur : {{Times New Roman|III<sup>+5</sup>}} ''do'' - ''mi'' - ''sol''♯ → {{Times New Roman|I<sup>6</sup>}} ''do'' - ''mi'' - ''la''), ou de l'accord de dominante ({{Times New Roman|III<sup>6</sup><sub>+3</sub>}} ''mi'' - ''sol''♯ - ''do'' → {{Times New Roman|V<sup>5</sup>}} ''mi'' - ''sol''♯ - ''si''). Il peut être aussi utilisé comme préparation à l'accord de sous-dominante (enchaînement {{Times New Roman|III}} → {{Times New Roman|IV}}). Par ailleurs, il a une constitution symétrique — c'est l'empilement de deux tierces majeures — et ses renversements ont les mêmes intervalles à l'enharmonie près (quinte augmentée/sixte mineure, tierce majeure/quarte diminuée). De ce fait, un même accord est commun, par renversement et à l'enharmonie près, à trois tonalités : le premier renversement de l'accord ''do'' - ''mi'' - ''sol''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur) est enharmonique à ''mi'' - ''sol''♯ - ''si''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''do''♯ mineur) ; le second renversement est enharmonique à ''la''♭ - ''do'' - ''mi'' ({{Times New Roman|III}}<sup>e</sup> degré de ''fa'' mineur).
=== Accords très utilisés ===
Les trois accords les plus utilisés sont les accords de tonique (degré {{Times New Roman|I}}), de sous-dominante ({{Times New Roman|IV}}) et de dominante ({{Times New Roman|V}}). Ils interviennent en particulier en fin de phrase, dans les cadences. L'accord de dominante sert souvent à introduire une modulation : la modulation commence sur l'accord de dominante de la nouvelle tonalité. On note que l'accord de sous-dominante est situé une quinte juste en dessous de la tonique, les accords de dominante et de sous-dominante sont donc symétriques.
En jazz, on utilise également très fréquemment l'accord de la sus-tonique (degré {{Times New Roman|II}}), souvent dans des progressions {{Times New Roman|II}} - {{Times New Roman|V}} (- {{Times New Roman|I}}). Rappelons que l'accord de sus-tonique a la fonction de sous-dominante.
=== Cadences et ''turnaround'' ===
Le terme « cadence » provient de l'italien ''cadenza'' et désigne la « chute », la fin d'un morceau ou d'une phrase musicale.
On distingue deux types de cadences :
* les cadences conclusive, qui créent une sensation de complétude ;
* les cadences suspensives, qui crèent une sensation d'attente.
==== Cadence parfaite ====
[[Fichier:Au clair de le lune cadence parfaite.midi|thumb|''Au clair de la lune'', harmonisé avec une cadence parfaite (italienne).]]
[[Fichier:Au clair de le lune mineur cadence parfaite.midi|thumb|''Idem'' mais en mode mineur harmonique.]]
La cadence parfaite est l'enchaînement de l'accord de dominante suivi de l'accord parfait : {{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}}, les deux accord étant à l'état fondamental. Elle donne une impression de stabilité et est donc très souvent utilisée pour conclure un morceau. C'est une cadence conclusive.
On peut aussi utiliser l'accord de septième de dominante, la dissonance introduisant une tension résolue par l'accord parfait : {{Times New Roman|V<sup>7</sup><sub>+</sub> - I<sup>5</sup>}}.
Elle est souvent précédée de l'accord construit sur le IV<sup>e</sup> degré, appelé « accord de préparation », pour former la cadence italienne : {{Times New Roman|IV<sup>5</sup> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}}.
Elle est également souvent précédée du second renversement de l'accord de tonique, qui est alors appelé « appoggiature de la cadence » : {{Times New Roman|I<sup>6</sup><sub>4</sub> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}} (on remarque que les accords {{Times New Roman|I}}<sup>6</sup><sub>4</sub> et {{Times New Roman|V}}<sup>5</sup> ont la basse en commun, et que l'on peut passer de l'un à l'autre par un mouvement conjoint sur les autres notes).
{{clear}}
==== Demi-cadence ====
[[Fichier:Au clair de le lune demi cadence.midi|thumb|''Au clair de la lune'', harmonisé avec une demi-cadence.]]
Une demi-cadence est une phrase ou un morceau se concluant sur l'accord construit sur le cinquième degré. Il provoque une sensation d'attente, de suspens. Il s'agit en général d'une succession {{Times New Roman|II - V}} ou {{Times New Roman|IV - V}}. C'est une cadence suspensive. On uilise rarement un accord de septième de dominante.
{{clear}}
==== Cadence rompue ou évitée ====
La cadence rompue, ou cadence évitée, est succession d'un accord de dominante et d'un accord de sus-dominante, {{Times New Roman|V}} - {{Times New Roman|VI}}. C'est une cadence suspensive.
==== Cadence imparfaite ====
Une cadence imparfaite est une cadence {{Times New Roman|V - I}}, comme la cadence parfaite, mais dont au moins un des deux accords est dans un état renversé.
==== Cadence plagale ====
La cadence plagale — du grec ''plagios'', oblique, en biais — est la succession de l'accord construit sur le quatrième degré, suivi de l'accord parfait : {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}. Elle peut être utilisée après une cadence parfaite ({{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}} - {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}). Elle donne un caractère solennel, voire religieux — elle est parfois appelée « cadence amen » —, elle a un côté antique qui rappelle la musique modale et médiévale<ref>{{lien web |url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/la-cadence-amen-ou-comment-se-dire-adieu-7191921 |titre=La cadence « Amen » ou comment se dire adieu |auteur=Max Dozolme (MAXXI Classique) |site=France Musique |date=2025-04-25 |consulté le=2025-04-25}}.</ref>.
C'est une cadence conclusive.
==== {{lang|en|Turnaround}} ====
[[Fichier:Au clair de le lune turnaround.midi|thumb|Au clair de la lune, harmonisé en style jazz : accords de 7{{e}}, anatole suivie d'un ''{{lang|en|turnaround}}'' ii-V-I.]]
Le terme ''{{lang|en|turnaround}}'' signifie revirement, retournement. C'est une succession d'accords que fait la transition entre deux parties, en créant une tension-résolution. Le ''{{lang|en|turnaround}}'' le plus courant est la succession {{Times New Roman|II - V - I}}.
On utilise également fréquemment l'anatole : {{Times New Roman|I - VI - II - V}}.
{| class="wikitable"
|+ Progressions typiques d'accords dans une tonalité majeure
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Cadence<br />parfaite<br /> {{Times New Roman|V - I}}
! scope="col" | Cadence<br />italienne<br />{{Times New Roman|IV - V - I}}
! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou IV - V}}
! scope="col" | Cadence<br />plagale<br />{{Times New Roman|IV - I}}
! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}}
! scope="col" | Anatole<br />{{Times New Roman|I - vi - ii - V}}
|-
|''Do'' majeur || || G - C || F - G - C || Dm - G ou F - G || F - C || Dm - G - C || C - Am - Dm - G
|-
|''Sol'' majeur || ''fa''♯ || D - G || C - D - G || Am - D ou C - D || C - G || Am - D - G || G - Em - Am - D
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || A - D || G - A - D || Em - A ou G - A || G - D || Em - A - D || D - Bm - Em - A
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || E - A || D - E - A || Bm - E ou D - E || D - A || Bm - E - A || A - F♯m - B - E
|-
| ''Fa'' majeur || ''si''♭ || C - F || B♭ - C - F || Gm - C ou B♭ - C || B♭ - F || Gm - C - F || F - Dm - Gm - C
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || F - B♭ || E♭ - F - B♭ || Cm - F ou E♭ - F || E♭ - B♭ || Cm - F - B♭ || B♭ - Gm - Cm - F
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || B♭ - E♭ || A♭ - B♭ - E♭ || Fm - B♭ ou A♭ - B♭ || A♭ - E♭ || Fm - B♭ - E♭ || Gm - Cm - Fm - B♭
|}
{| class="wikitable"
|+ Progressions typiques d'accords dans une tonalité mineure
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Cadence<br />parfaite<br />{{Times New Roman|V - i}}
! scope="col" | Cadence<br />italienne<br />{{Times New Roman|iv - V - i}}
! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou iv - V}}
! scope="col" | Cadence<br />plagale<br />{{Times New Roman|iv - i}}
! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}}
! scope="col" | Anatole<br />{{Times New Roman|i - VI - ii - V}}
|-
| ''La'' mineur<br />harmonique || || E - Am || Dm - E - Am || B° - E ou Dm - E || Dm - Am || B° - E - Am || Am - F - B° - E
|-
| ''Mi'' mineur<br />harmonique || ''fa''♯ || B - Em || Am - B - Em || F♯° - B ou Am - B || Am - Em || F♯° - B - Em || Em - C - F♯° - B
|-
| ''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || F♯ - Bm || Em - F♯ - Bm || C♯° - F♯ ou Em - F♯ || Em - Bm || C♯° - F♯ - Bm || Bm - G - C♯° - F♯
|-
| ''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || C♯ - F♯m || Bm - C♯ - F♯m || G♯° - C♯ ou Bm - C♯ || Bm - F♯m || G♯° - C♯ - F♯m || A+ - D - G♯° - C♯
|-
| ''Ré'' mineur<br />harmonique || ''si''♭ || A - Dm || Gm - A - Dm || E° - A ou Gm - A || Gm - Dm || E° - A - Dm || Dm - B♭ - E° - A
|-
| ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || D - Gm || Cm - D - Gm || A° - D ou Cm - D || Cm - Gm|| A° - D - Gm || Gm - E♭ - A° - D
|-
| ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || G - Cm || Fm - G - Cm || D° - G ou Fm - G || Fm - Dm || D° - G - Cm || Cm - A♭ - D° - G
|}
==== Exemple : ''La Mer'' ====
: {{lien web
| url = https://www.youtube.com/watch?v=PXQh9jTwwoA
| titre = Charles Trenet - La mer (Officiel) [Live Version]
| site = YouTube
| auteur = Charles Trenet
| consulté le = 2020-12-24
}}
Le début de ''La Mer'' (Charles Trenet, 1946) est en ''do'' majeur et est harmonisé par l'anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (C - Am - Dm - G<sup>7</sup>) sur deux mesures, jouée deux fois ({{Times New Roman|1=<nowiki>|I-vi|ii-V</nowiki><sup>7</sup><nowiki>|</nowiki>}} × 2). Viennent des variations avec les progressions {{Times New Roman|I-III-vi-V<sup>7</sup>}} (C - E - Am - G<sup>7</sup>) puis la « progression ’50s » (voir plus bas) {{Times New Roman|I-vi-IV-VI<sup>7</sup>}} (C - Am - F - A<sup>7</sup>, on remarque que {{Times New Roman|IV}}/F est le relatif majeur du {{Times New Roman|ii}}/Dm de l'anatole), jouées chacune une fois sur deux mesure ; puis cette première partie se conclut par une demie cadence {{Times New Roman|ii-V<sup>7</sup>}} sur une mesure puis une dernière anatole sur trois mesures ({{Times New Roman|1=<nowiki>|I-vi|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}}). Cela constitue une première partie « A » sur douze mesures qui se termine par une demi-cadence ({{Times New Roman|ii-V<sup>7</sup>}}) qui appelle une suite. Cette partie A est jouée une deuxième fois mais la fin est modifiée pour la transition : les deux dernières mesures {{Times New Roman|<nowiki>|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}} deviennent {{Times New Roman|<nowiki>|ii-V</nowiki><sup>7</sup><nowiki>|I|</nowiki>}} (|Dm-G7|C|), cette partie « A’ » se conclut donc par une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}).
Le morceau passe ensuite en tonalité de ''mi'' majeur, donc une tierce au dessus de ''do'' majeur, sur six mesures. Cette partie utilise une progression ’50s {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (E - C♯m - A - B<sup>7</sup>), qui est rappelons-le une variation de l'anatole, l'accord {{Times New Roman|ii}} (Fm) étant remplacé par son relatif majeur {{Times New Roman|IV}} (A). Cette anatole modifiée est jouée deux fois puis la partie en ''mi'' majeur se conclut par l'accord parfait {{Times New Roman|I}} joué sur deux mesures (|E|E|), on a donc, avec la mesure précédente, avec une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}).
Suivent ensuite six mesures en ''sol'' majeur, donc à nouveau une tierce au dessus de ''mi'' majeur. Elle comporte une progression {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (G - Em - C - D<sup>7</sup>), donc anatole avec substitution du {{Times New Roman|ii}}/Am par son relatif majeur {{Times New Roman|VI}}/C (progression ’50s), puis une anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (G - Em - Am - D<sup>7</sup>) et deux mesure sur la tonique {{Times New Roman|I-I<sup>7</sup>}} (G - G<sup>7</sup>), formant à nouveau une cadence parfaite. La fin sur un accord de septième, dissonant, appelle une suite.
Cette partie « B » de douze mesures comporte donc deux parties similaires « B1 » et « B2 » qui forment une marche harmonique (montée d'une tierce).
Le morceau se conclut par une reprise de la partie « A’ » et se termine donc par une cadence parfaite.
Nous avons une structure A-A’-B-A’ sur 48 mesures, proche la forme AABA étudiée plus loin.
Donc ''La Mer'' est un morceau structuré autour de l'anatole avec des variations (progression ’50s, substitution du {{Times New Roman|ii}} par son relatif majeur {{Times New Roman|IV}}) et comportant une marche harmonique dans sa troisième partie. Les parties se concluent par des ''{{lang|en|turnarounds}}'' sous la forme d'une cadence parfaite ou, pour la partie A, par une demi-cadence.
{| border="1" rules="rows" frame="hsides"
|+ Structure de ''La Mer''
|- align="center"
|
| colspan="12" | ''do'' majeur
|
|- align="center"
! scope="row" rowspan=2 | A
| colspan="2" | anatole
| colspan="2" | //
| colspan="2" | variation
| colspan="2" | ’50s
| ½ c.
| colspan="3" | anatole
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii}} || <nowiki>|</nowiki> {{Times New Roman|V<sup>7</sup>}} || <nowiki>|</nowiki>
|- align="center"
! scope="row" rowspan="2" | A’
| colspan="2" | anatole
| colspan="2" | //
| colspan="2" | variation
| colspan="2" | ’50s
| ½ c.
| colspan="2" | anatole
| c.p.
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki>
|- align="center"
|
| colspan="6" | B1 : ''mi'' majeur
| colspan="6" background="lightgray" | B2 : ''sol'' majeur
|
|- align="center"
! scope="row" rowspan="2" | B
| colspan="2" | ’50s
| colspan="2" | //
|colspan="2" | c.p.
| colspan="2" | ’50s
| colspan="2" | //
|colspan="2" | c.p.
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I<sup>7</sup>}} || <nowiki>|</nowiki>
|-
! scope="row" | A’
| colspan="12" |
|
|}
=== Progression blues ===
La musique blues est apparue dans les années 1860. Elle est en général bâtie sur une grille d'accords ''({{lang|en|changes}})'' immuable de douze mesures ''({{lang|en|twelve-bar blues}})''. C'est sur cet accompagnement qui se répète que s'ajoute la mélodie — chant et solo. Cette structure est typique du blues et se retrouve dans ses dérivés comme le rock 'n' roll.
Le rythme est toujours un rythme ternaire syncopé ''({{lang|en|shuffle, swing, groove}}, ''notes inégales'')'' : la mesure est à quatre temps, mais la noire est divisée en noire-croche en triolet, ou encore triolet de croche en appuyant la première et la troisième.
La mélodie se construit en général sur une gamme blues de six degrés (gamme pentatonique mineure avec une quarte augmentée), mais bien que la gamme soit mineure, l'harmonie est construite sur la gamme majeure homonyme : un blues en ''fa'' a une mélodie sur la gamme de ''fa'' mineur, mais une harmonie sur la gamme de ''fa'' majeur. La grille d'accord comporte les accords construits sur les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la gamme majeure homonyme. Les accords sont souvent des accords de septième (donc avec une tierce majeure et une septième mineure), il ne s'agit donc pas d'une harmonisation de gamme diatonique (puisque la septième est majeure sur l'accord de tonique).
Par exemple, pour un blues en ''do'' :
* accord parfait de do majeur, C ({{Times New Roman|I}}<sup>er</sup> degré) ;
* accord parfait de fa majeur, F ({{Times New Roman|IV}}<sup>e</sup> degré) ;
* accord parfait de sol majeur, G ({{Times New Roman|V}}<sup>e</sup> degré).
Il existe quelques morceaux harmonisés avec des accords mineurs, comme par exemple ''As the Years Go Passing By'' d'Albert King (Duje Records, 1959).
La progression blues est organisée en trois blocs de quatre mesures ayant les fonctions suivantes (voir ci-dessus ''[[#Harmonie fonctionnelle|Harmonie fonctionnelle]]'') :
* quatre mesures toniques ;
* quatre mesures sous-dominantes ;
* quatre mesures dominantes.
La forme la plus simple, que Jeff Gardner appelle « forme A », est la suivante :
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, forme A
|-
! scope="row" | Tonique
| width="50px" | I
| width="50px" | I
| width="50px" | I
| width="50px" | I
|-
! scope="row" | Sous-domminante
| width="50px" | IV
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
! scope="row" | Dominante
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | V
|}
La progression {{Times New Roman|I-V}} des deux dernières mesures forment le ''{{lang|en|turnaround}}'', la demie cadence qui lance le cycle suivant. Nous présentons ci-dessous un exemple typique de ligne de basse ''({{lang|en|walking bass}})'' pour le ''{{lang|en|turnaround}}'' d'un blues en ''la'' :
[[Fichier:Turnaround classique blues en la.svg|Exemple typique de ligne de basse pour un ''turnaround'' de blues en ''la''.]]
[[Fichier:Blues mi harmonie elementaire.midi|thumb|Blues en ''mi'', harmonisé de manière élémentaire avec une ''{{lang|en|walking bass}}''.]]
Vous pouvez écouter ci-contre une harmonisation typique d'un blues en ''mi''. Les accords sont exécutés par une basse marchante ''({{lang|en|walking bass}})'', qui joue une arpège sur la triade avec l'ajout d'une sixte majeure et d'une septième mineure, et par une guitare qui joue un accord de puissance ''({{lang|en|power chord}})'', qui n'est composé que de la fondamentale et de la quinte juste, avec une sixte en appoggiature.
La forme B s'obtient en changeant la deuxième mesure : on joue un degré {{Times New Roman|IV}} au lieu d'un degré {{Times New Roman|I}}. La progression {{Times New Roman|I-IV}} sur les deux premières mesures est appelé ''{{lang|en|quick change}}''.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, forme B
|-
| width="50px" | I
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
| width="50px" | IV
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | V
|}
Par exemple, ''Sweet Home Chicago'' (Robert Johnson, 1936) est un blues en ''fa'' ; sa grille d'accords, aux variations près, suit une forme B :
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression de ''Sweet Home Chicago''
|-
| width="50px" | F
| width="50px" | B♭
| width="50px" | F
| width="50px" | F
|-
| width="50px" | B♭
| width="50px" | B♭
| width="50px" | F
| width="50px" | F
|-
| width="50px" | C7
| width="50px" | B♭7
| width="50px" | F7
| width="50px" | C7
|}
: Écouter {{lien web
| url =https://www.youtube.com/watch?v=dkftesK2dck
| titre = Robert Johnson "Sweet Home Chicago"
| auteur = Michal Angel
| site = YouTube
| date = 2007-12-09 | consulté le = 2020-12-17
}}
Les formes C et D s'obtiennent à partir des formes A et B en changeant le dernier accord par un accord sur le degré {{Times New Roman|I}}, ce qui forme une cadence plagale.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, formes C et D
|-
| colspan="4" | …
|-
| colspan="4" | …
|-
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|}
L'harmonie peut être enrichie, notamment en jazz. Voici par exemple une grille du blues souvent utilisés en bebop.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Exemple de progression de blues bebop sur une base de forme B
|-
| width="60px" | I<sup>7</sup>
| width="60px" | IV<sup>7</sup>
| width="60px" | I<sup>7</sup>
| width="60px" | V–<sup>7</sup> <nowiki>|</nowiki> I<sup>7</sup>
|-
| width="60px" | IV<sup>7</sup>
| width="60px" | IV<sup>7</sup>
| width="60px" | I<sup>7</sup>
| width="60px" | VI<sup>7 ♯9 ♭13</sup>
|-
| width="60px" | II–<sup>7</sup>
| width="60px" | V<sup>7</sup>
| width="60px" | V<sup>7</sup> <nowiki>|</nowiki> IV<sup>7</sup>
| width="60px" | II–<sup>7</sup> <nowiki>|</nowiki> V<sup>7</sup>
|}
On peut aussi trouver des blues sur huit mesures, sur seize mesures comme ''Watermelon Man'' de Herbie Hancock (album ''Takin' Off'', Blue Note, 1962) ou ''Let's Dance'' de Jim Lee (interprété par Chris Montez, Monogram, 1962)
* {{lien web
|url= https://www.dailymotion.com/video/x5iduwo
|titre=Herbie Hancock - Watermelon Man (1962)
|auteur=theUnforgettablesTv
|site=Dailymotion
|date=2003 |consulté le=2021-02-09
}}
* {{lien web
|url=https://www.youtube.com/watch?v=6JXshurYONc
|titre=Let's Dance
|auteur=Chris Montez
|site=YouTube
|date=2016-08-06 |consulté le=2021-02-09
}}
À l'inverse, certains blues peuvent avoir une structure plus simple que les douze mesure ; par exemple ''Hoochie Coochie Man'' de Willie Dixon (interprété par Muddy Waters sous le titre ''Mannish Boy'', Chicago Blues, 1954) est construit sur un seul accord répété tout le long de la chanson.
* {{lien web
|url=https://www.dailymotion.com/video/x5iduwo
|titre=Muddy Waters - Hoochie Coochie Man
|auteur=Muddy Waters
|site=Dailymotion
|date=2012 | consulté le=2021-02-09
}}
=== Cadence andalouse ===
La cadence andalouse est une progression de quatre accords, descendant par mouvement conjoint :
* en mode de ''mi'' (mode phrygien) : {{Times New Roman|IV}} - {{Times New Roman|III}} - {{Times New Roman|II}} - {{Times New Roman|I}} ;<br />par exemple en ''mi'' phrygien : Am - G - F - E ; en ''do'' phrygien : Fm - E♭ - D♭ - C ;<br />on notera que le degré {{Times New Roman|III}} est diésé dans l'accord final (ou bécarre s'il est bémol dans la tonalité) ;
* en mode mineur : {{Times New Roman|I}} - {{Times New Roman|VII}} - {{Times New Roman|VI}} - {{Times New Roman|V}} ;<br />par exemple en ''la'' mineur : Am - G - F - E ; en ''do'' mineur : Cm - B♭ - A♭ - m ;<br />comme précédemment, on notera que le degré {{Times New Roman|VII}} est diésé dans l'accord final.
=== Progressions selon le cercle des quintes ===
[[Fichier:Cercle quintes degres tonalite majeure.svg|vignette|Cercle des quinte justes (parcouru dans le sens des aiguilles d'une montre) des degrés d'une tonalité majeure.]]
La progression {{Times New Roman|V-I}} est la cadence parfaite, mais on peut aussi l'employer au milieu d'un morceau. Cette progression étant courte, sa répétition crée de la lassitude ; on peut la compléter par d'autres accords séparés d'une quinte juste, en suivant le « cercle des quintes » : {{Times New Roman|I-V-IX}}, la neuvième étant enharmonique de la seconde, on obtient {{Times New Roman|I-V-II}}.
On peut continuer de décrire le cercle des quintes : {{Times New Roman|I-V-II-VI}}, on obtient l'anatole dans le désordre ; on peut à l'inverse étendre les quintes vers la gauche, {{Times New Roman|IV-I-V-II-VI}}.
En musique populaire, on trouve fréquemment une progression fondée sur les accord {{Times New Roman|I}}, {{Times New Roman|IV}}, {{Times New Roman|V}} et {{Times New Roman|VI}}, popularisée dans les années 1950. La « progression années 1950 », « progression ''{{lang|en|fifties ('50)}}'' » ''({{lang|en|'50s progression}})'' est dans l'ordre {{Times New Roman|I-VI-IV-V}}. On trouve aussi cette progression en musique classique. Si la tonalité est majeure, la triade sur la sus-dominante est mineure, les autres sont majeures, on notera donc souvent {{Times New Roman|I-vi-IV-V}}. On peut avoir des permutations circulaires (le dernier accord venant au début, ou vice-versa) : {{Times New Roman|vi-IV-V-I}}, {{Times New Roman|IV-V-I-vi}} et {{Times New Roman|V-I-vi-IV}}.
{| class="wikitable"
|+ Accords selon la tonalité
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" style="font-family:Times New Roman" | I
! scope="col" style="font-family:Times New Roman" | IV
! scope="col" style="font-family:Times New Roman" | V
! scope="col" style="font-family:Times New Roman" | vi
|-
|''Do'' majeur || || C || F || G || Am
|-
|''Sol'' majeur || ''fa''♯ || G || C || D || Em
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || D || G || A || Bm
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A || D || E || F♯m
|-
| ''Fa'' majeur || ''si''♭ || F || B♭ || C || Dm
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭ || E♭ || F || Gm
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭ || A♭ || B♭ || Cm
|}
Par exemple, en tonalité de ''do'' majeur, la progression {{Times New Roman|I-vi-IV-V}} sera C-Am-F-G.
Il existe d'autres progressions utilisant ces accords mais dans un autre ordre, typiquement {{Times New Roman|I–IV–vi–V}} ou une de ses permutations circulaires : {{Times New Roman|IV–vi–V-I}}, {{Times New Roman|vi–V-I-IV}} ou {{Times New Roman|V-I-IV-vi}}. Ou dans un autre ordre.
PV Nova l'illustre dans plusieurs de ses « expériences » dans la version {{Times New Roman|vi-V-IV-I}}, soit Am-G-F-C, ou encore {{Times New Roman|vi-IV-I-V}}, soit Am-F-C-G :
: {{lien web
| url = https://www.youtube.com/watch?v=w08LeZGbXq4
| titre = Expérience n<sup>o</sup> 6 — La Happy Pop
| auteur = PV Nova
| site = YouTube
| date = 2011-08-20 | consulté le = 2020-12-13
}}
et cela devient un gag récurrent avec son « chapeau des accords magiques qu'on nous ressort à toutes les sauces »
: {{lien web
| url = https://www.youtube.com/watch?v=VMY_vc4nZAU
| titre = Expérience n<sup>o</sup> 14 — La Soupe dou Brasil
| auteur = PV Nova
| site = YouTube
| date = 2012-10-03 | consulté le = 2020-12-17
}}
Cette récurrence est également parodiée par le groupe The Axis of Awesome avec ses « chansons à quatre accords » ''({{lang|en|four-chords song}})'', dans une sketch où ils mêlent 47 chansons en utilisant l'ordre {{Times New Roman|I-V-vi-IV}} :
: {{lien web
| url = https://www.youtube.com/watch?v=oOlDewpCfZQ
| titre = 4 Chords | Music Videos | The Axis Of Awesome
| auteur = The Axis of Awesome
| site = YouTube
| date = 2011-07-20 | consulté le = 2020-12-17
}}
{{boîte déroulante/début|titre=Chansons mêlées dans le sketch}}
# Journey : ''Don't Stop Believing'' ;
# James Blunt : ''You're Beautiful'' ;
# Black Eyed Peas : ''Where Is the Love'' ;
# Alphaville : ''Forever Young'' ;
# Jason Mraz : ''I'm Yours'' ;
# Train : ''Hey Soul Sister'' ;
# The Calling : ''Wherever You Will Go'' ;
# Elton John : ''Can You Feel The Love Tonight'' (''Le Roi lion'') ;
# Akon : ''Don't Matter'' ;
# John Denver : ''Take Me Home, Country Roads'' ;
# Lady Gaga : ''Paparazzi'' ;
# U2 : ''With Or Without You'' ;
# The Last Goodnight : ''Pictures of You'' ;
# Maroon Five : ''She Will Be Loved'' ;
# The Beatles : ''Let It Be'' ;
# Bob Marley : ''No Woman No Cry'' ;
# Marcy Playground : ''Sex and Candy'' ;
# Men At Work : ''Land Down Under'' ;
# thème de ''America's Funniest Home Videos'' (équivalent des émissions ''Vidéo Gag'' et ''Drôle de vidéo'') ;
# Jack Johnson : ''Taylor'' ;
# Spice Girls : ''Two Become One'' ;
# A Ha : ''Take On Me'' ;
# Green Day : ''When I Come Around'' ;
# Eagle Eye Cherry : ''Save Tonight'' ;
# Toto : ''Africa'' ;
# Beyonce : ''If I Were A Boy'' ;
# Kelly Clarkson : ''Behind These Hazel Eyes'' ;
# Jason DeRulo : ''In My Head'' ;
# The Smashing Pumpkins : ''Bullet With Butterfly Wings'' ;
# Joan Osborne : ''One Of Us'' ;
# Avril Lavigne : ''Complicated'' ;
# The Offspring : ''Self Esteem'' ;
# The Offspring : ''You're Gonna Go Far Kid'' ;
# Akon : ''Beautiful'' ;
# Timberland featuring OneRepublic : ''Apologize'' ;
# Eminem featuring Rihanna : ''Love the Way You Lie'' ;
# Bon Jovi : ''It's My Life'' ;
# Lady Gaga : ''Pokerface'' ;
# Aqua : ''Barbie Girl'' ;
# Red Hot Chili Peppers : ''Otherside'' ;
# The Gregory Brothers : ''Double Rainbow'' ;
# MGMT : ''Kids'' ;
# Andrea Bocelli : ''Time To Say Goodbye'' ;
# Robert Burns : ''Auld Lang Syne'' ;
# Five for fighting : ''Superman'' ;
# The Axis of Awesome : ''Birdplane'' ;
# Missy Higgins : ''Scar''.
{{boîte déroulante/fin}}
Vous pouvez par exemple jouer les accords C-G-Am-F ({{Times New Roman|I-V-vi-IV}}) et chanter dessus ''{{lang|en|Let It Be}}'' (Paul McCartney, The Beattles, 1970) ou ''Libérée, délivrée'' (Robert Lopez, ''La Reine des neiges'', 2013).
La progression {{Times New Roman|I-V-vi-IV}} est considérée comme « optimiste » tandis que sa variante {{Times New Roman|iv-IV-I-V}} est considérée comme « pessimiste ».
On peut voir la progression {{Times New Roman|I-vi-IV-V}} comme une variante de l'anatole {{Times New Roman|I-vi-ii-V}}, obtenue en remplaçant l'accord de sustonique {{Times New Roman|ii}} par l'accord de sous-dominante {{Times New Roman|IV}} (son relatif majeur, et degré ayant la même fonction).
==== Exemples de progression selon le cercle des quintes en musique classique ====
[[Fichier:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.midi|vignette|Dietrich Buxtehude, Psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]]
Cette progression selon la cercle des quintes, sous la forme {{Times New Roman|I-vi-IV-V}}, apparaît déjà au {{pc|xvii}}<sup>e</sup> siècle dans le psaume 42 ''Quem ad modum desiderat cervis'' (BuxVW92) de Dietrich Buxtehude (1637-1707). Le morceau est en ''fa'' majeur, la progression d'accords est donc F-Dm-B♭-C.
: {{lien web
| url = https://www.youtube.com/watch?v=8FmV9l1RqSg
| titre = D. Buxtehude - Quemadmodum desiderat cervus, BuxWV 92
| auteur = Longobardo
| site = YouTube
| date = 2013-04-06 | consulté la = 2021-01-01
}}
[[File:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.svg|vignette|450x450px|center|Dietrich Buxtehude, psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]]
{{clear}}
[[Fichier:JSBach BWV140 cantate 4 mesures.midi|vignette|J.-S. Bach, cantate BWV140, quatre premières mesures.]]
On la trouve également dans l'ouverture de la cantate ''{{lang|de|Wachet auf, ruft uns die Stimme}}'' de Jean-Sébastien Bach (BWV140, 1731). Le morceau est en ''mi''♭ majeur, la progression d'accords est donc E♭-Cm-A♭<sup>6</sup>-B♭.
[[Fichier:JSBach BWV140 cantate 4 mesures.svg|vignette|center|J.-S. Bach, cantate BWV140, quatre premières mesures.|alt=|517x517px]]
{{clear}}
[[Fichier:Mozart K310 Sonate8 mesures 45 a 49.midi|vignette|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]]
La même progression est utilisée par Mozart, par exemple dans le premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778), la progression d'accords est C-Am-F-G qui correspond à la progression {{Times New Roman|III-i-VI-VII}} de ''la'' mineur, mais à la progression {{Times New Roman|I-vi-IV-V}} de la gamme relative, ''do'' majeur .
[[Fichier:Mozart K310 Sonate8 mesures 45 a 49.svg|vignette|center|500px|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]]
=== Substitution tritonique ===
Un des accords les plus utilisés est donc l'accord de septième de dominante, {{Times New Roman|V<sup>7</sup><sub>+</sub>}} qui contient les degrés {{Times New Roman|V}}, {{Times New Roman|VII}}, {{Times New Roman|II}} ({{Times New Roman|IX}}) et {{Times New Roman|IV}}({{Times New Roman|XI}}) ; par exemple, en tonalité de ''do'' majeur, l'accord de ''sol'' septième (G<sup>7</sup>) contient les notes ''sol''-''si''-''ré''-''fa''. Si l'on prend l'accord dont la fondamentale est trois tons (triton) au-dessus ou en dessous — l'octave contenant six tons, on arrive sur la même note —, {{Times New Roman|♭II<sup>7</sup>}}, ici ''ré''♭ septième (D♭<sup>7</sup>), celui-ci contient les notes ''ré''♭-''fa''-''la''♭-''do''♭, cette dernière note étant l'enharmonique de ''si''. Les deux accords G<sup>7</sup> et D♭<sup>7</sup> ont donc deux notes en commun : le ''fa'' et le ''si''/''do''♭.
Il est donc fréquent en jazz de substituer l'accord {{Times New Roman|V<sup>7</sup><sub>+</sub>}} par l'accord {{Times New Roman|♭II<sup>7</sup>}}. Par exemple, la progression {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|V<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}} devient {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|♭II<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}}. C'est un procédé courant de réharmonisation (le fait de remplacer un accord par un autre dans un morceau existant).
Les six substitutions possibles sont donc : C<sup>7</sup>↔F♯<sup>7</sup> - D♭<sup>7</sup>↔G<sup>7</sup> - D<sup>7</sup>↔A♭<sup>7</sup> - E♭<sup>7</sup>↔A<sup>7</sup> - E<sup>7</sup>↔B♭<sup>7</sup> - F<sup>7</sup>↔B<sup>7</sup>.
[[Fichier:Übermäsiger Terzquartakkord.jpg|vignette|Exemple de cadence parfaite en ''do'' majeur avec substitution tritonique (sixte française).]]
Dans l'accord D♭<sup>7</sup>, si l'on remplace le ''do''♭ par son ''si'' enharmonique, on obtient un accord de sixte augmentée : ''ré''♭-''fa''-''la''♭-''si''. Cet accord est utilisé en musique classique depuis la Renaissance ; on distingue en fait trois accords de sixte augmentée :
* sixte française ''ré''♭-''fa''-''sol''-''si'' ;
* sixte allemande : ''ré''♭-''fa''-''la''♭-''si'' ;
* sixte italienne : ''ré''♭-''fa''-''si''.
Par exemple, le ''Quintuor en ''ut'' majeur'' de Franz Schubert (1828) se termine par une cadence parfaite dont l'accord de dominante est remplacé par une sixte française ''ré''♭-''fa''-''si''-''sol''-''si'' (''ré''♭ aux violoncelles, ''fa'' à l'alto, ''si''-''sol'' aux seconds violons et ''si'' au premier violon).
[[Fichier:Schubert C major Quintet ending.wav|vignette|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]]
[[Fichier:Schubert C major Quintet ending.png|vignette|center|upright=2.5|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]]
=== Autres accords de substitution ===
Substituer un accord consiste à utiliser un accord provenant d'une tonalité étrangère à la tonalité en cours. À la différence d'une modulation, la substitution est très courte et ne donne pas l'impression de changer de tonalité ; on a juste un sentiment « étrange » passager. Un court passage dans une autre tonalité est également appelée « emprunt ».
Nous avons déjà vu plusieurs méthodes de substitution :
* utilisation d'une note étrangère : une note étrangère — note de passage, appoggiature, anticipation, retard… — crée momentanément un accord hors tonalité ; en musique classique, ceci n'est pas considéré comme un accord en propre, mais en jazz, on parle « d'accord de passage » et « d'accord suspendu » ;
* utilisation d'une dominante secondaire : l'accord de dominante secondaire est hors tonalité ; le but ici est de faire une cadence parfaite, mais sur un autre degré que la tonique de la tonalité en cours ;
* la substitution tritonique, vue ci-dessus, pour remplacer un accord de septième de dominante.
Une dernière méthode consiste à remplacer un accord par un accord d'une gamme de même tonique, mais d'un autre mode ; on « emprunte » ''({{lang|en|borrow}})'' l'accord d'un autre mode. Par exemple, substituer un accord de la tonalité de ''do'' majeur par un accord de la tonalité de ''do'' mineur ou de ''do'' mode de ''mi'' (phrygien).
Donc en ''do'' majeur, on peut remplacer un accord de ''ré'' mineur septième (D<sub>m</sub><sup>7</sup>) par un accord de ''ré'' demi-diminué (D<sup>⌀</sup>, D<sub>m</sub><sup>7♭5</sup>) qui est un accord appartenant à la donalité de ''la'' mineur harmonique.
=== Forme AABA ===
La forme AABA est composée de deux progressions de huit mesures, notées A et B ; cela représente trente-deux mesures au total, on parle donc souvent en anglais de la ''{{lang|en|32-bars form}}''. C'est une forme que l'on retrouve dans de nombreuses chanson de comédies musicales de Broadway comme ''Have You Met Miss Jones'' (''{{lang|en|I'd Rather Be Right}}'', 1937), ''{{lang|en|Over the Rainbow}}'' (''Le Magicien d'Oz'', Harold Harlen, 1939), ''{{lang|en|All the Things You Are}}'' (''{{lang|en|Very Warm for may}}'', 1939).
Par exemple, la version de ''{{lang|en|Over the Rainbow}}'' chantée par Judy Garland est en ''la''♭ majeur et la progression d'accords est globalement :
* A (couplet) : A♭-Fm | Cm-A♭ | D♭ | Cm-A♭ | D♭ | D♭-F | B♭-E♭ | A♭
* B (pont) : A♭ | B♭m | Cm | D♭ | A♭ | B♭-G | Cm-G | B♭m-E♭
soit en degrés :
* A : {{Times New Roman|<nowiki>I-vi | iii-I | IV | iii-IV | IV | IV-vi | II-V | I</nowiki>}}
* B : {{Times New Roman|<nowiki>I | ii | iii | IV | I | II-VII | iii-VII | ii-V</nowiki>}}
Par rapport aux paroles de la chanson, on a
* A : couplet 1 ''« {{lang|en|Somewhere […] lullaby}} »'' ;
* A : couplet 2 ''« {{lang|en|Somewhere […] really do come true}} »'' ;
* B : pont ''« {{lang|en|Someday […] you'll find me}} »'' ;
* A : couplet 3 ''« {{lang|en|Somewhere […] oh why can't I?}} »'' ;
: {{lien web
| url = https://www.youtube.com/watch?v=1HRa4X07jdE
| titre = Judy Garland - Over The Rainbow (Subtitles)
| site = YouTube
| auteur = Overtherainbow
| consulté le = 2020-12-17
}}
Une mise en œuvre de la forme AABA couramment utilisée en jazz est la forme anatole (à le pas confondre avec la succession d'accords du même nom), en anglais ''{{lang|en|rythm changes}}'' car elle s'inspire du morceau ''{{lang|en|I Got the Rythm}}'' de George Gerschwin (''Girl Crazy'', 1930) :
* A : {{Times New Roman|I–vi–ii–V}} (succession d'accords « anatole ») ;
* B : {{Times New Roman|III<sup>7</sup>–VI<sup>7</sup>–II<sup>7</sup>–V<sup>7</sup>}} (les fondamentales forment une succession de quartes, donc parcourent le « cercle des quintes » à l'envers).
Par exemple, ''I Got the Rythm'' étant en ''ré''♭ majeur, la forme est :
* A : D♭ - B♭m - E♭m - A♭
* B : F7 - B♭7 - E♭7 - A♭7
=== Exemples ===
==== Début du Largo de la symphonie du Nouveau Monde ====
[[File:Largo nouveau monde 5 1res mesures.svg|vignette|Partition avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]]
[[File:Largo nouveau monde 5 1res mesures.midi|vignette|Fichier son avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]]
Nous avons reproduit ci-contre les cinq premières mesure du deuxième mouvement Largo de la symphonie « Du Nouveau Monde » (symphonie n<sup>o</sup> 9 d'Antonín Dvořák, 1893). Cliquez sur l'image pour l'agrandir.
Vous pouvez écouter cette partie jouée par un orchestre symphonique :
* {{lien web
|url =https://www.youtube.com/watch?v=y2Nw9r-F_yQ?t=565
|titre = Dvorak Symphony No.9 "From the New World" Karajan 1966
|site=YouTube (Seokjin Yoon)
|consulté le=2020-12-11
}} (à 9 min 25), par le Berliner Philharmoniker, dirigé par Herbert von Karajan (1966) ;
* {{lien web
|url = https://www.youtube.com/watch?v=ASlch7R1Zvo
|titre=Dvořák: Symphony №9, "From The New World" - II - Largo
|site=YouTube (diesillamusicae)
|consulté le=2020-12-11
}} : Wiener Philharmoniker, dirigé par Herbert von Karajan (1985).
{{clear}}
Cette partie fait intervenir onze instruments monodiques (ne jouant qu'une note à la fois) : des vents (trois bois, sept cuivres) et une percussion. Certains de ces instruments sont transpositeurs (les notes sur la partition ne sont pas les notes entendues). Jouées ensemble, ces onze lignes mélodiques forment des accords.
Pour étudier cette partition, nous réécrivons les parties des instruments transpositeurs en ''do'' et les parties en clef d’''ut'' en clef de ''fa''. Nous regroupons les parties en clef de ''fa'' d'un côté et les parties en clef de ''sol'' d'un autre.
{{boîte déroulante|Résultat|contenu=[[File:Largo nouveau monde 5 1res mesures transpositeurs en do.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde, en do.]]}}
Nous pouvons alors tout regrouper sous la forme d'un système de deux portées clef de ''fa'' et clef de ''sol'', comme une partition de piano.
{{boîte déroulante|Résultat|contenu=
[[Fichier:Largo nouveau monde 5 1res mesures accords.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords.]]
}}
[[Fichier:Largo nouveau monde 5 1res mesures accords.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords.]]
{{clear}}
Ensuite, nous ne gardons que la basse et les notes médium. Nous changeons éventuellement certaines notes d'octave afin de n'avoir que des superpositions de tierce ou de quinte (état fondamental des accords, en faisant ressortir les notes manquantes).
{{boîte déroulante|Résultat|contenu=
[[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords simplifiés.]]
}}
[[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords simplifiés.]]
Vous pouvez écouter cette partie jouée par un quintuor de cuivres (trompette, bugle, cor, trombone, tuba), donc avec des accords de cinq notes :
: {{lien web
|url=https://www.youtube.com/watch?v=pWfe60nbvjA
|titre = Largo from The New World Symphony by Dvorak
|site=YouTube (The Chamberlain Brass)
|consulté le=2020-12-11
}} : The American Academy of Arts & Letters in New York City (2017).
Nous allons maintenant chiffrer les accords.
Pour établir la basse chiffrée, il nous faut déterminer le parcours harmonique. Pour le premier accord, les tonalités les plus simples avec un ''sol'' dièse sont ''la'' majeur et ''fa'' dièse mineur ; comme le ''mi'' est bécarre, nous retenons ''la'' majeur, il s'agit donc d'un accord de quinte sur la dominante (les accords de dominante étant très utilisés, cela nous conforte dans notre choix). Puis nous avons un ''si'' bémol, nous pouvons être en ''fa'' majeur ou en ''ré'' mineur ; nous retenons ''fa'' majeur, c'est donc le renversement d'un accord sur le degré {{Times New Roman|II}}.
Dans la deuxième mesure, nous revenons en ''la'' majeur, puis, avec un ''la'' et un ''ré'' bémols, nous sommes en ''la'' bémol majeur ; nous avons donc un accord de neuvième incomplet sur la sensible, ou un accord de onzième incomplet sur la dominante.
Dans la troisième mesure, nous passons en ''ré'' majeur, avec un accord de dominante. Puis, nous arrivons dans la tonalité principale, avec le renversement d'un accord de dominante sans tierce suivi d'un accord de tonique. Nous avons donc une cadence parfaite, conclusion logique d'une phrase.
La progression des accords est donc :
{| class="wikitable"
! scope="row" | Tonalité
| ''la'' M - ''fa'' M || ''la'' M - ''la''♭ M || ''ré'' M - ''ré''♭ M || ''ré''♭ M
|-
! scope="row" | Accords
| {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|II}}<sup>6</sup><sub>4</sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|“V”}}<sup>9</sup><sub><s>5</s></sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|V}}<sup>+4</sup> || {{Times New Roman|I}}<sup>5</sup>
|}
Dans le chiffrage jazz, nous avons donc :
* une triade de ''mi'' majeur, E ;
* une triade de ''sol'' majeur avec un ''ré'' en basse : G/D ;
* à nouveau un E ;
* un accord de ''sol'' neuvième diminué incomplet, avec un ''ré'' bémol en basse : G dim<sup>9</sup>/D♭ ;
* un accord de ''la'' majeur, A ;
* un accord de ''la'' bémol septième avec une ''sol'' bémol à la basse : A♭<sup>7</sup>/G♭ ;
* la partie se conclue par un accord parfait de ''ré''♭ majeur, D♭.
Soit une progression E - G/D | E - G dim<sup>9</sup>/D♭ | A - A♭<sup>7</sup>/G♭ | D♭.
[[Fichier:Largo nouveau monde 5 1res mesures accords chiffres.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde en accords simplifiés.]]
{{clear}}
==== Thème de Smoke on the Water ====
Le morceau ''Smoke on the Water'' du groupe Deep Purple (album ''Machine Head'', 1972) possède un célèbre thème, un riff ''({{lang|en|rythmic figure}})'', joué à la guitare sous forme d'accords de puissance ''({{lang|en|power chords}})'', c'est-à-dire des accords sans tierce. Le morceau est en tonalité de ''sol'' mineur naturel (donc avec un ''fa''♮) avec ajout de la note bleue (''{{lang|en|blue note}}'', quinte diminuée, ''ré''♭), et les accords composant le thème sont G<sup>5</sup>, B♭<sup>5</sup>, C<sup>5</sup> et D♭<sup>5</sup>, ce dernier accord étant l'accord sur la note bleue et pouvant être considéré comme une appoggiature (indiqué entre parenthèse ci-après). On a donc ''a priori'', sur les deux premières mesures, une progression {{Times New Roman|I-III-IV}} puis {{Times New Roman|I-III-(♭V)-IV}}. Durant la majeure partie du thème, la guitare basse tient la note ''sol'' en pédale.
{{note|En jazz, la qualité « <sup>5</sup> » indique que l'on n'a que la quinte (et donc pas la tierce), contrairement à la notation de basse chiffrée.}}
: {{lien web
| url = https://www.dailymotion.com/video/x5ili04
| titre = Deep Purple — Smoke on the Water (Live at Montreux 2006)
| auteur = Deep Purple
| site = Dailymotion
| date = 2016 | consulté le = 2020-12-31
}}
Cependant, cette progression forme une mélodie, on peut donc plus la voir comme un contrepoint, la superposition de deux voies ayant un mouvement conjoint, joué par un seul instrument, la guitare, la voie 2 étant jouée une quarte juste en dessous de la voie 1 (la quarte juste descendante étant le renversement de la quinte juste ascendante) :
* voie 1 (aigu) : | ''sol'' - ''si''♭ - ''do'' | ''sol'' - ''si''♭ - (''ré''♭) - ''do'' | ;
* voie 2 (grave) : | ''ré'' - ''fa'' - ''sol'' | ''ré'' - ''fa'' - (''la''♭) - ''sol'' |.
En se basant sur la basse (''sol'' en pédale), nous pouvons considérer que ces deux mesures sont accompagnées d'un accord de Gm<sup>7</sup> (''sol''-''si''♭-''ré''-''fa''), chaque accord de la mélodie comprenant à chaque fois au moins une note de cet accord à l'exception de l'appogiature.
{| class="wikitable"
|+ Mise en évidence des notes de l'accord Gm<sup>7</sup>
|-
! scope="row" | Accords
| G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup>
|-
! scope="row" | Voie 1
| '''''sol''''' || '''''si''♭''' || ''do''
|-
! scope="row" | Voie 2
| '''''ré''''' || '''''fa''''' || '''''sol'''''
|-
! scope="row" | Basse
| '''''sol''''' || '''''sol''''' || '''''sol'''''
|}
Sur les deux mesures suivantes, la basse varie et suit les accords de la guitare avec un retard sur le dernier accord :
{| class="wikitable"
|+ Voies sur les mesure 3-4 du thème
|-
! scope="row" | Accords
| G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> || B♭<sup>5</sup> || G<sup>5</sup>
|-
! scope="row" | Voie 1
| ''sol'' || ''si''♭ || ''do'' || ''si''♭ || ''sol''
|-
! scope="row" | Voie 2
| ''ré'' || ''fa'' || ''sol'' || ''fa'' || ''ré''
|-
! scope="row" | Basse
| ''sol'' || ''sol'' || ''do'' || ''si''♭ || ''si''♭-''sol''
|}
Le couplet de cette chanson est aussi organisé sur une progression de quatre mesures, la guitare faisant des arpèges sur les accords G<sup>5</sup> (''sol''-''ré''-''sol'') et F<sup>5</sup> (''fa''-''do''-''fa'') :
: | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-F<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> |
soit une progression {{Times New Roman|<nowiki>| I-I | I-I | I-VII | I-I |</nowiki>}}. Nous pouvons aussi harmoniser le riff du thème sur cette progression, avec un accord F (''fa''-''la''-''do'') ; nous pouvons aussi nous rappeler que l'accord sur le degré {{Times New Roman|VII}} est plus volontiers considéré comme un accord de septième de dominante {{Times New Roman|V<sup>7</sup>}}, soit ici un accord Dm<sup>7</sup> (''ré''-''fa''-''la''-''do''). On peut donc considérer la progression harmonique sur le thème :
: | Gm-Gm | Gm-Gm | Gm-F ou Dm<sup>7</sup> | Gm-Gm |.
Cette analyse permet de proposer une harmonisation enrichie du morceau, tout en se rappelant qu'une des forces du morceau initial est justement la simplicité de sa structure, qui fait ressortir la virtuosité des musiciens. Nous pouvons ainsi comparer la version album à la version concert avec orchestre ou à la version latino de Pat Boone. À l'inverse, le groupe Psychostrip, dans une version grunge, a remplacé les accords par une ligne mélodique :
* le thème ne contient plus qu'une seule voie (la guitare ne joue pas des accords de puissance) ;
* dans les mesures 9 et 10, la deuxième guitare joue en contrepoint de type mouvement inverse, qui est en fait la voie 2 jouée en miroir ;
* l'arpège sur le couplet est remplacé par une ligne mélodique en ostinato sur une gamme blues.
{| class="wikitable"
|+ Contrepoint sur les mesures 9 et 10
|-
! scope="row" | Guitare 1
| ''sol'' ↗ ''si''♭ ↗ ''do''
|-
! scope="row" | Guitare 2
| ''sol'' ↘ ''fa'' ↘ ''ré''
|}
* {{lien web
| url = https://www.dailymotion.com/video/x5ik234
| titre = Deep Purple — Smoke on the Water (In Concert with the London Symphony Orchestra, 1999)
| auteur = Deep Purple
| site = Dailymotion
| date = 2016 | consulté le = 2020-12-31
}}
* {{lien web
| url = https://www.youtube.com/watch?v=MtUuNzVROIg
| titre = Pat Boone — Smoke on the Water (In a Metal Mood, No More Mr. Nice Guy, 1997)
| auteur = Orrore a 33 Giri
| site = YouTube
| date = 2019-06-24 | consulté le = 2020-12-31
}}
* {{lien web
| url = https://www.youtube.com/watch?v=n7zLlZ8B0Bk
| titre = Smoke on the Water (Heroes, 1993)
| auteur = Psychostrip
| site = YouTube
| date = 2018-06-20 | consulté le = 2020-12-31
}}
== Accords et improvisation ==
Nous avons vu précédemment (chapitre ''[[../Gammes et intervalles#Modes et improvisation|Gammes et intervalles > Modes et improvisation]]'') que le choix d'un mode adapté permet d'improviser sur un accord. L'harmonisation des gammes permet, en inversant le processus, d'étendre notre palette : il suffit de repérer l'accord sur une harmonisaiton de gamme, et d'utiliser cette gamme-là, dans le mode correspondant du degré de l'accord (voir ci-dessus ''[[#Harmonisation par des accords de septième|Harmonisation par des accords de septième]]'').
Par exemple, nous avons vu que l'accord sur le septième degré d'une gamme majeure était un accord demi-diminué ; nous savons donc que sur un accord demi-diminué, nous pouvons improviser sur le mode correspondant au septième degré, soit le mode de ''si'' (locrien).
Un accord de septième de dominante étant commun aux deux tonalités homonymes (par exemple ''fa'' majeur et ''fa'' mineur pour un ''do''<sup>7</sup><sub>+</sub> / C<sup>7</sup>), nous pouvons utiliser le mode de ''sol'' de la gamme majeure (mixolydien) ou de la gamme mineure mineure (mode phrygien dominant, ou phrygien espagnol) pour improviser. Mais l'accord de septième de dominante est aussi l'accord au début d'une grille blues ; on peut donc improviser avec une gamme blues, même si la tierce est majeure dans l'accord et mineure dans la gamme.
[[Fichier:Mode improvisation accords do complet.svg]]
== Autres accords courants ==
[[fichier:Cluster cdefg.png|vignette|Agrégat ''do - ré - mi - fa - sol''.]]
Nous avons vu précédemment l'harmonisation des tonalités majeures et mineures harmoniques par des triades et des accords de septième ; certains accords étant rarement utilisés (l'accord sur le degré {{Times New Roman|III}} et, pour les tonalités mineures harmoniques, l'accord sur la tonique), certains accords étant utilisés comme des accords sur un autre degré (les accords sur la sensible étant considérés comme des accords de dominante sans fondamentale).
Dans l'absolu, on peut utiliser n'importe quelle combinaison de notes, jusqu'aux agrégats, ou ''{{lang|en|clusters}}'' (mot anglais signifiant « amas », « grappe ») : un ensemble de notes contigües, séparées par des intervalles de seconde. Dans la pratique, on reste souvent sur des accords composés de superpositions de tierces, sauf dans le cas de transitions (voir la section ''[[#Notes étrangères|Notes étrangère]]'').
=== En musique classique ===
On utilise parfois des accords dont les notes ne sont pas dans la tonalité (hors modulation). Il peut s'agir d'accords de passage, de notes étrangères, par exemple utilisant un chromatisme (mouvement conjoint par demi-tons).
Outre les accords de passage, les autres accords que l'on rencontre couramment en musique classique sont les accords de neuvième, et les accords de onzième et treizième sur tonique. Ces accords sont simplement obtenus en continuant à empiler les tierces. Il n'y a pas d'accord d'ordre supérieur car la quinzième est deux octaves au-dessus de la fondamentale.
Comme pour les accords de septième, on distingue les accords de neuvième de dominante et les accords de neuvième d'espèce. Dans le cas de la neuvième de dominante, il y a une différence entre les tonalités majeures et mineures : l'intervalle de neuvième est respectivement majeur et mineur. Les chiffrages des renversements peuvent donc différer. Comme pour les accords de septième de dominante, on considère que les accords de septième sur le degré {{Times New Roman|VI}} sont en fait des accords de neuvième de dominante sans fondamentale.
Les accords de neuvième d'espèce sont en général préparés et résolus. Préparés : la neuvième étant une note dissonante (c'est à une octave près la seconde de la fondamentale), l'accord qui précède doit contenir cette note, mais dans un accord consonant ; la neuvième est donc commune avec l'accord précédent. Résolus : la dissonance est résolue en abaissant la neuvième par un mouvement conjoint. Par exemple, en tonalité de ''do'' majeur, si l'on veut utiliser un accord de neuvième d'espèce sur la tonique ''(do - mi - sol - si - ré)'', on peut utiliser avant un accord de dominante ''(sol - si - ré)'' en préparation puis un accord parfait sur le degré {{Times New Roman|IV}} ''(fa - la - do)'' en résolution ; nous avons donc sur la voie la plus aigüe la succession ''ré'' (consonant) - ''ré'' (dissonant) - ''do'' (consonant).
On rencontre également parfois des accords de onzième et de treizième. On omet en général la tierce, car elle est dissonante avec la onzième. L'accord le plus fréquemment rencontré est l'accord sur la tonique : on considère alors que c'est un accord sur la dominante que l'on a enrichi « par le bas », en ajoutant une quinte inférieure. par exemple, dans la tonalité de ''do'' majeur, l'accord ''do - sol - si - ré - fa'' est considéré comme un accord de septième de dominante sur tonique, le degré étant noté « {{Times New Roman|V}}/{{Times New Roman|I}} ». De même pour l'accord ''do - sol - si - ré - fa - la'' qui est considéré comme un accord de neuvième de dominante sur tonique.
=== En jazz ===
En jazz, on utilise fréquemment l'accord de sixte à la place de l'accord de septième majeure sur la tonique. Par exemple, en ''do'' majeur, on utilise l'accord C<sup>6</sup> ''(do - mi - sol - la)'' à la place de C<sup>Δ</sup> ''(do - mi - sol - si)''. On peut noter que C<sup>6</sup> est un renversement de Am<sup>7</sup> et pourrait donc se noter Am<sup>7</sup>/C ; cependant, le fait de le noter C<sup>6</sup> indique que l'on a bien un accord sur la tonique qui s'inscrit dans la tonalité de ''do'' majeur (et non, par exemple, de ''la'' mineur naturelle) — par rapport à l'harmonie fonctionnelle, on remarquera que Am<sup>7</sup> a une fonction tonique, l'utilisation d'un renversement de Am<sup>7</sup> à la place d'un accord de C<sup>Δ</sup> est donc logique.
Les accords de neuvième, onzième et treizième sont utilisés comme accords de septième enrichis. Le chiffrage suit les règles habituelles : on ajoute un « 9 », un « 11 » ou un « 13 » au chiffrage de l'accord de septième.
On utilise également des accords dits « suspendus » : ce sont des accords de transition qui sont obtenus en prenant une triade majeure ou mineure et en remplaçant la tierce par la quarte juste (cas le plus fréquent) ou la seconde majeure. Plus particulièrement, lorsque l'on parle simplement « d'accord suspendu » sans plus de précision, cela désigne l'accord de neuvième avec une quarte suspendue, noté « 9sus4 » ou simplement « sus ».
== L'harmonie tonale ==
L'harmonie tonale est un ensemble de règle assez strictes qui s'appliquent dans la musique savante européenne, de la période baroque à la période classique classique ({{pc|xiv}}<sup>e</sup>-{{pc|xviii}}<sup>e</sup> siècle). Certaines règles sont encore largement appliquées dans divers styles musicaux actuels, y compris populaire (rock, rap…), d'autres sont au contraire ignorées (par exemple, un enchaînement de plusieurs accords de même qualité forme un mouvement parallèle, ce qui est proscrit en harmonie tonale). De nos jours, on peut voir ces règles comme des règles « de bon goût », et leur application stricte comme une manière de composer « à la manière de ».
Précédemment, nous avons vu la progression des accords. Ci-après, nous abordons aussi la manière dont les notes de l'accord sont réparties entre plusieurs voix, et comment on construit chaque voix.
=== Concepts fondamentaux ===
; Consonance
: Les intervalles sont considérés comme « plus ou moins consonants » :
:* consonance parfaite : unisson, quinte et octave ;
:* consonance mixte (parfaite dans certains contextes, imparfaite dans d'autres) : quarte ;
:* consonance imparfaite : tierce et sixte ;
:* dissonance : seconde et septième.
; Degrés
: Certains degrés sont considérés comme « forts », « meilleurs », ce sont les « notes tonales » : {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (sous-dominante) et {{Times New Roman|V}} (dominante).
[[Fichier:Mouvements harmoniques.svg|vignette|upright=0.75|Mouvements harmoniques.]]
; Mouvements
: Le mouvement décrit la manière dont les voix évoluent les unes par rapport aux autres :
:# Mouvement parallèle : les voix sont séparées par un intervalle constant.
:# Mouvement oblique : une voix reste constante, c'est le bourdon ; l'autre monte ou descend.
:# Mouvement contraire : une voix descend, l'autre monte.
:# Échange de voix : les voix échangent de note ; les mélodies se croisent mais on a toujours le même intervalle harmonique.
{{clear}}
=== Premières règles ===
; Règle du plus court chemin
: Quand on passe d'un accord à l'autre, la répartition des notes se fait de sorte que chaque voix fait le plus petit mouvement possible. Notamment : si les deux accords ont des notes en commun, alors les voix concernées gardent la même note.
: Les deux voix les plus importantes sont la voix aigüe — soprano — et la voix la plus grave — basse. Ces deux voix sont relativement libres : la voix de soprano a la mélodie, la voix de basse fonde l'harmonie. La règle du plus court chemin s'applique surtout aux voix intermédiaires ; si l'on a des mouvements conjoints, ou du moins de petits intervalles — c'est le sens de la règle du plus court chemin —, alors les voix sont plus faciles à interpréter. Cette règle évite également que les voix n'empiètent l'une sur l'autre (voir la règle « éviter le croisement des voix »).
; Éviter les consonances parfaites consécutives
:* Lorsque deux voix sont à l'unisson ou à l'octave, elles ne doivent pas garder le même intervalle, l'effet serait trop plat.
:* Lorsque deux voix sont à la quarte ou à la quinte, elles ne doivent pas garder le même intervalle, car l'effet est trop dur.
: Pour éviter cela, lorsque l'on part d'un intervalle juste, on a intérêt à pratiquer un mouvement contraire aux voix qui ne gardent pas la même note, ou au moins un mouvement direct : les voix vont dans le même sens, mais l'intervalle change.
: Notez que même avec le mouvement contraire, on peut avoir des consonances parfaites consécutives, par exemple si une voix fait ''do'' aigu ↗ ''sol'' aigu et l'autre ''sol'' médium ↘ ''do'' grave.
: L'interdiction des consonances parfaites consécutives n'a pas été toujours appliquée, le mouvement parallèle strict a d'ailleurs été le premier procédé utilisé dans la musique religieuse au {{pc|x}}<sup>e</sup> siècle. On peut par exemple utiliser des quintes parallèles pour donner un style médiéval au morceau. On peut également utiliser des octaves parallèles sur plusieurs notes afin de créer un effet de renforcement de la mélodie.
: Par ailleurs, les consonances parfaites consécutives sont acceptées lorsqu'il s'agit d'une cadence (transition entre deux parties ou bien conclusion du morceau).
; Éviter le croisement des voix
: Les voix sont organisées de la plus grave à la plus aigüe. Deux voix n'étant pas à l'unisson, celle qui est plus aigüe ne doit pas devenir la plus grave et ''vice versa''.
; Soigner la partie soprano
: Comme c'est celle qu'on entend le mieux, c'est en général celle qui porte la mélodie principale. On lui applique des règles spécifiques :
:# Si elle chante la sensible dans un accord de dominante ({{Times New Roman|V}}), alors elle doit monter à la tonique, c'est-à-dire que la note suivante sera la tonique située un demi-ton au dessus.
:# Si l'on arrive à une quinte ou une octave entre les parties basse et soprano par un mouvement direct, alors sur la partie soprano, le mouvement doit être conjoint. On doit donc arriver à cette situation par des notes voisines au soprano.
; Préférer certains accords
: Les deux degrés les plus importants sont la tonique ({{Times New Roman|I}}) et la dominante ({{Times New Roman|V}}), les accords correspondants ont donc une importance particulière.
: À l'inverse, l'accord de sensible ({{Times New Roman|VII}}) n'est pas considéré comme ayant une fonction harmonique forte. On le considère comme un accord de dominante affaibli. En tonalité mineure, on évite également l'accord de médiante ({{Times New Roman|III}}).
: Donc on utilise en priorité les accords de :
:# {{Times New Roman|I}} et {{Times New Roman|V}}.
:# Puis {{Times New Roman|II}}, {{Times New Roman|IV}}, {{Times New Roman|VI}} ; et {{Times New Roman|III}} en mode majeur.
:# On évite {{Times New Roman|VII}} ; et {{Times New Roman|III}} en mode mineur.
; Préférer certains enchaînements
: Les enchaînements d'accord peuvent être classés par ordre de préférence. Par ordre de préférence décroissante (du « meilleur » au « moins bon ») :
:# Meilleurs enchaînements : quarte ascendante ou descendante. Notons que la quarte est le renversement de la quinte, on a donc des enchaînements stables et naturels, mais avec un intervalle plus court qu'un enchaînement de quintes.
:# Bons enchaînements : tierce ascendante ou descendante. Les accords consécutifs ont deux notes en commun.
:# Enchaînements médiocres : seconde ascendante ou descendante. Les accords sont voisins, mais ils n'ont aucune note en commun. On les utilise de préférence en mouvement ascendant, et on utilise surtout les enchaînements {{Times New Roman|IV}}-{{Times New Roman|V}}, {{Times New Roman|V}}-{{Times New Roman|VI}} et éventuellement {{Times New Roman|I}}-{{Times New Roman|II}}.
:# Les autres enchaînements sont à éviter.
: On peut atténuer l'effet d'un enchaînement médiocre en plaçant le second accord sur un temps faible ou bien en passant par un accord intermédiaire.
[[Fichier:Progression Vplus4 I6.svg|thumb|Résolution d'un accord de triton (quarte sensible) vers l'accord de sixte de la tonique.]]
; La septième descend par mouvement conjoint
: Dans un accord de septième de dominante, la septième — qui est donc le degré {{Times New Roman|IV}} — descend par mouvement conjoint — elle est donc suivie du degré {{Times New Roman|III}}.
: Corolaire : un accord {{Times New Roman|V}}<sup>+4</sup> se résout par un accord {{Times New Roman|I}}<sup>6</sup> : on a bien un enchaînement {{Times New Roman|V}} → {{Times New Roman|I}}, et la 7{{e}} (degré {{Times New Roman|IV}}), qui est la basse de l'accord {{Times New Roman|V}}<sup>+4</sup>, descend d'un degré pour donner la basse de l'accord {{Times New Roman|I}}<sup>6</sup> (degré {{Times New Roman|III}}).
{{clear}}
[[Fichier:Progression I64 V7plus I5.svg|thumb|Accord de sixte et de quarte cadentiel.]]
; Un accord de sixte et quarte est un accord de passage
: Le second renversement d'un accord parfait est soit une appoggiature, soit un accord de passage, soit un accord de broderie.
: S'il s'agit de l'accord de tonique {{Times New Roman|I}}<sup>6</sup><sub>4</sub>, c'est « accord de sixte et quarte de cadence », l'appoggiature de l'accord de dominante de la cadence parfaite.
{{clear}}
Mais il faut appliquer ces règles avec discernement. Par exemple, la voix la plus aigüe est celle qui s'entend le mieux, c'est donc elle qui porte la mélodie principale. Il est important qu'elle reste la plus aigüe. La voix la plus grave porte l'harmonie, elle pose les accords, il est donc également important qu'elle reste la plus grave. Ceci a deux conséquences :
# Ces deux voix extrêmes peuvent avoir des intervalles mélodiques importants et donc déroger à la règle du plus court chemin : la voix aigüe parce que la mélodie prime, la voix de basse parce que la progression d'accords prime.
# Les croisements des voix intermédiaires sont moins critiques.
Par ailleurs, si l'on applique strictement toutes les règles « meilleurs accords, meilleurs enchaînements », on produit un effet conventionnel, stéréotypé. Il est donc important d'utiliser les solutions « moins bonnes », « médiocres » pour apporter de la variété.
Ajoutons que les renversements d'accords permettent d'avoir plus de souplesse : on reste sur le même accord, mais on enrichit la mélodie sur chaque voix.
Le ''Bolero'' de Maurice Ravel (1928) brise un certain nombre de ces règles. Par exemple, de la mesure 39 à la mesure 59, la harpe joue des secondes. De la mesure 149 à la mesure 165, les piccolo jouent à la sixte, dans des mouvement strictement parallèle, ce qui donne d'ailleurs une sonorité étrange. À partir de la mesure 239, de nombreux instruments jouent en mouvement parallèles (piccolos, flûtes, hautbois, cor, clarinettes et violons).
=== Application ===
[[Fichier:Harmonisation possible de frere jacques exercice.svg|vignette|Exercice : harmoniser ''Frère Jacques''.]]
Harmoniser ''Frère Jacques''.
Nous considérons un morceau à quatre voix : basse, ténor, alto et soprano. La soprano chante la mélodie de ''Frère Jacques''. L'exercice consiste à proposer l'écriture des trois autres voix en respectant les règles énoncées ci-dessus. Pour simplifier, nous ajoutons les contraintes suivantes :
* toutes les voix chantent des blanches ;
* nous nous limitons aux accords de quinte (accords de trois sons composés d'une tierce et d'une quinte) sans avoir recours à leurs renversements (accords de sixte, accords de sixte et de quarte).
Les notes à gauche de la portée indiquent la tessiture (ou ambitus), l'amplitude que peut chanter la voix.
{{clear}}
{{boîte déroulante/début|titre=Solution possible}}
[[Fichier:Harmonisation possible de frere jacques solution.svg|vignette|Harmonisation possible de ''Frère Jacques'' (solution de l'exercice).]]
Il n'y a pas qu'une solution possible.
Le premier accord doit contenir un ''do''. Nous sommes manifestement en tonalité de ''do'' majeur, nous proposons de commencer par l'accord parfait de ''do'' majeur, I<sup>5</sup>.
Le deuxième accord doit comporter un ''ré''. Si nous utilisons l'accord de quinte de ''ré'', nous allons créer une quinte parallèle. Nous pourrions utiliser un renversement, mais nous nous imposons de chercher un autre accord. Il peut s'agir de l'accord ''si''<sup>5</sup> ''(si-ré-fa)'' ou de l'accord de ''sol''<sup>5</sup> ''(sol-si-ré)''. La dernière solution permet d'utiliser l'accord de dominante qui est un accord important de la tonalité. La règle du plus court chemin imposerait le ''sol'' grave pour la partie de basse, mais cela est proche de la limite du chanteur, nous préférons passer au ''sol'' aigu, plus facile à chanter. Nous vérifions qu'il n'y a pas de quinte parallèle : l'intervalle ascendant ''do-sol'' (basse-alto) devient ''sol-si'' (3<sup>ce</sup>), l'intervalle descendant ''do-sol'' (soprano-alto) devient ''ré-si'' (3<sup>ce</sup>).
De la même manière, pour le troisième accord, nous ne pouvons pas passer à un accord de ''la''<sup>5</sup> pour éviter une quinte parallèle. Nous avons le choix entre ''do''<sup>5</sup> ''(do-mi-sol)'' et ''mi''<sup>5</sup> ''(mi-sol-si)''. Nous préférons revenir à l'accord de fondamental, solution très stable (l'enchaînement {{Times New Roman|V}}-{{Times New Roman|I}} formant une cadence parfaite).
Pour le quatrième accord, nous pourrions rester sur l'accord parfait de ''do'' mais cela planterait en quelque sorte la fin du morceau puisque l'on resterait sur la cadence parfaite ; or, nous connaissons le morceau et savons qu'il n'est pas fini. Nous choisissons l'accord de ''la''<sup>5</sup> qui est une sixte ascendante ({{Times New Roman|I}}-{{Times New Roman|VI}}).
Nos aurions pu répartir les voix différemment. Par exemple :
* alto : ''sol''-''si''-''sol''-''do'' ;
* ténor : ''mi''-''ré''-''mi''-''mi''.
{{boîte déroulante/fin}}
[[Fichier:Harmonisation possible de frere jacques.midi|vignette|Fichier son correspondant.]]
{{clear}}
== Annexe ==
=== Accords en musique classique ===
Un accord est un ensemble de notes jouées simultanément. Il peut s'agir :
* de notes jouées par plusieurs instruments ;
* de notes jouées par un même instrument : piano, clavecin, orgue, guitare, harpe (la plupart des instruments à clavier et des instruments à corde).
Pour deux notes jouées simultanément, on parle d'intervalle « harmonique » (par opposition à l'intervalle « mélodique » qui concerne les notes jouées successivement).
Les notes répétées à différentes octaves ne changent pas la nature de l'accord.
La musique classique considère en général des empilements de tierces ; un accord de trois notes sera constitué de deux tierces successives, un accord de quatre notes de trois tierces…
Lorsque tous les intervalles sont des intervalles impairs — tierces, quintes, septièmes, neuvièmes, onzièmes, treizièmes… — alors l'accord est dit « à l'état fondamental » (ou encore « primitif » ou « direct »). La note de la plus grave est appelée « fondamentale » de l'accord. Lorsque l'accord comporte un ou des intervalles pairs, l'accord est dit « renversé » ; la note la plus grave est appelée « basse ».
De manière plus générale, l'accord est dit à l'état fondamental lorsque la basse est aussi la fondamentale. On a donc un état idéal de l'accord (état canonique) — un empilement strict de tierces — et l'état réel de l'accord — l'empilement des notes réellement jouées, avec d'éventuels redoublements, omissions et inversions ; et seule la basse indique si l'accord est à l'état fondamental ou renversé.
Le chiffrage dit de « basse continue » ''({{lang|it|basso continuo}})'' désigne la représentation d'un accord sous la forme d'un ou plusieurs chiffres arabes et éventuellement d'un chiffre romain.
==== Accords de trois notes ====
En musique classique, les seuls accords considérés comme parfaitement consonants, c'est-à-dire sonnant agréablement à l'oreille, sont appelés « accords parfaits ». Si l'on prend une tonalité et un mode donné, alors l'accord construit par superposition es degrés I, III et V de cette gamme porte le nom de la gamme qui l'a généré.
[[fichier:Accord do majeur chiffre.svg|vignette|upright=0.5|Accord parfait de ''do'' majeur chiffré.]]
Par exemple :
* « l'accord parfait de ''do'' majeur » est composé des notes ''do'', ''mi'' et ''sol'' ;
* « l'accord parfait de ''la'' mineur » est composé des notes ''la'', ''do'' et ''mi''.
Un accord parfait majeur est donc composé, en partant de la fondamentale, d'une tierce majeure et d'une quinte juste. Un accord parfait mineur est composé d'une tierce mineure et d'une quinte juste.
L'accord parfait à l'état fondamental est appelé « accord de quinte » et est simplement chiffré « 5 » pour indiquer la quinte.
On peut également commencer un accord sur sa deuxième ou sa troisième note, en faisant monter celle(s) qui précède(nt) à l'octave suivante. On parle alors de « renversement d'accord » ou d'accord « renversé ».
[[Fichier:Accord do majeur renversements chiffre.svg|vignette|upright=0.75|Accord parfait de ''do'' majeur et ses renversements, chiffrés.]]
Par exemple,
* le premier renversement de l'accord parfait de ''do'' majeur est :<br /> ''mi'', ''sol'', ''do'' ;
* le second renversement de l'accord parfait de do majeur est :<br /> ''sol'', ''do'', ''mi''.
Les notes conservent leur nom de « fondamentale », « tierce » et « quinte » malgré le changement d'ordre. La note la plus grave est appelée « basse ».
Dans le cas du premier renversement, le deuxième note est la tierce de la basse (la note la plus grave) et la troisième note est la sixte ; le chiffrage en chiffres arabes est donc « 6 » (puisque l'on omet la tierce) et l'accord est appelé « accord de sixte ». Pour le deuxième renversement, les intervalles sont la quarte et la sixte, le chiffrage est donc « 6-4 » et l'accord est appelé « accord de sixte et de quarte ».
Dans tous les cas, on chiffre le degré on considérant la fondamentale, par exemple {{Times New Roman|I}} si l'accord est construit sur la tonique de la gamme.
Les autres accords de trois notes que l'on rencontre sont :
* l'accord de quinte diminuée, constitué d'une tierce mineure et d'une quinte diminuée ; lorsqu'il est construit sur le septième degré d'une gamme, on considère que c'est un accord de septième de dominante sans fondamentale (voir plus bas), le degré est donc indiqué « “{{Times New Roman|V}}” » (cinq entre guillemets) et non « {{Times New Roman|VII}} » ;
* l'accord de quinte augmenté : il est composé d'une tierce majeure et qu'une quinte augmentée.
Dans le tableau ci-dessous,
* « m » désigne un intervalle mineur ;
* « M » un intervalle majeur ou le mode majeur ;
* « J » un intervalle juste ;
* « d » un intervalle diminué ;
* « A » un intervalle augmenté ;
* « mh » le mode mineur harmonique ;
* « ma » le mode mineur ascendant ;
* « md » le mode mineur descendant.
{| class="wikitable"
|+ Accords de trois notes
! scope="col" rowspan="2" | Nom
! scope="col" rowspan="2" | 3<sup>ce</sup>
! scope="col" rowspan="2" | 5<sup>te</sup>
! scope="col" rowspan="2" | État fondamental
! scope="col" rowspan="2" | 1<sup>er</sup> renversement
! scope="col" rowspan="2" | 2<sup>nd</sup> renversement
! scope="col" colspan="4"| Construit sur les degrés
|-
! scope="col" | M
! scope="col" | mh
! scope="col" | ma
! scope="col" | md
|-
| Accord parfait<br /> majeur || M || J
| accord de quinte || accord de sixte || accord de<br />sixte et de quarte
| {{Times New Roman|I, IV, V}} || {{Times New Roman|V, VI}} || {{Times New Roman|IV, V}} || {{Times New Roman|III, VI, VII}}
|-
| Accord parfait<br /> mineur || m || J
| accord de quinte || accord de sixte || accord de<br />sixte et de quarte
| {{Times New Roman|II, III, VI}} || {{Times New Roman|I, IV}} || {{Times New Roman|I, II}} || {{Times New Roman|I, IV, V}}
|-
| Accord de<br />quinte diminuée || m || d
| accord de<br />quinte diminuée || accord de<br />sixte sensible<br />sans fondamentale || accord de triton<br />sans fondamentale
| {{Times New Roman|VII (“V”)}} || {{Times New Roman|II, VII (“V”)}} || {{Times New Roman|VI, VII (“V”)}} || {{Times New Roman|II}}
|-
| Accord de<br />quinte augmentée || M || A
| accord de<br />quinte augmentée || accord de sixte<br />et de tierce sensible || accord de sixte et de quarte<br />sur sensible
| || {{Times New Roman|III}} || {{Times New Roman|III}} ||
|}
==== Accords de quatre notes ====
Les accords de quatre notes sont des accord composés de trois tierces superposées. La dernière note étant le septième degré de la gamme, on parle aussi d'accords de septième.
Ces accords sont dissonants : ils contiennent un intervalle de septième (soit une octave montante suivie d'une seconde descendante). Ils laissent donc une impression de « tension ».
Il existe sept différents types d'accords, ou « espèces ». Citons l'accord de septième de dominante, l'accord de septième mineure et l'accord de septième majeure.
===== L'accord de septième de dominante =====
[[Fichier:Accord 7e dominante do majeur renversements chiffre.svg|vignette|Accord de septième de dominante de ''do'' majeur et ses renversements, chiffrés.]]
L'accord de septième de dominante est l'empilement de trois tierces à partir de la dominante de la gamme, c'est-à-dire du {{Times New Roman|V}}<sup>e</sup> degré. Par exemple, l'accord de septième de dominante de ''do'' majeur est l'accord ''sol''-''si''-''ré''-''fa'', et l'accord de septième de dominante de ''la'' mineur est ''mi''-''sol''♯-''si''-''ré''. L'accord de septième de dominante dont la fondamentale est ''do'' (''do''-''mi''-''sol''-''si''♭) appartient à la gamme de ''fa'' majeur.
Que le mode soit majeur ou mineur, il est composé d'une tierce majeure, d'une quinte juste et d'une septième mineure (c'est un accord parfait majeur auquel on ajoute une septième mineure). C'est de loin l'accord de septième le plus utilisé ; il apparaît au {{pc|xvii}}<sup>e</sup> en musique classique.
Dans son état fondamental, son chiffrage est {{Times New Roman|V 7/+}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}). Le signe plus indique la sensible.
Son premier renversement est appelé « accord de quinte diminuée et sixte » et est noté {{Times New Roman|V 6/<s>5</s>}} (ou {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}).
Son deuxième renversement est appelé « accord de sixte sensible », puisque la sixte de l'accord est la sensible de la gamme, et est noté {{Times New Roman|V +6}} (ou {{Times New Roman|V<sup>+6</sup>}}).
Son troisième renversement est appelé « accord de quarte sensible » et est noté {{Times New Roman|V +4}} (ou {{Times New Roman|V<sup>+4</sup>}}).
[[Fichier:Accord 7e dominante sans fondamentale do majeur renversements chiffre.svg|vignette|Accord de septième de dominante sans fondamentale de ''do'' majeur et ses renversements, chiffrés.]]
On utilise aussi l'accord de septième de dominante sans fondamentale ; c'est alors un accord de trois notes.
Dans son état fondamental, c'est un « accord de quinte diminuée » placé sur le {{Times New Roman|VII}}<sup>e</sup> degré (mais c'est bien un accord construit sur le {{Times New Roman|V}}<sup>e</sup> degré), noté {{Times New Roman|“V” <s>5</s>}} (ou {{Times New Roman|“V”<sup><s>5</s></sup>}}). Notez les guillemets qui indiquent que la fondamentale V est absente.
Dans son premier renversement, c'est un « accord de sixte sensible sans fondamentale » noté {{Times New Roman|“V” +6/3}} (ou {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}}).
Dans son second renversement, c'est un « accord de triton sans fondamentale » (puisque le premier intervalle est une quarte augmentée qui comporte trois tons) noté {{Times New Roman|“V” 6/+4}} (ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}).
Notons qu'un accord de septième de dominante n'a pas toujours la dominante pour fondamentale : tout accord composé d'une tierce majeure, d'une quinte juste et d'une septième mineure est un accord de septième de dominante et est chiffré {{Times New Roman|<sup>7</sup><sub>+</sub>}}, quel que soit le degré sur lequel il est bâti (certaines notes peuvent avoir une altération accidentelle).
===== Les accords de septième d'espèce =====
Les autres accords de septièmes sont dits « d'espèce ».
L'accord de septième mineure est l'accord de septième formé sur la fondamentale d'une gamme mineure ''naturelle''. Par exemple, l'accord de septième mineure de ''la'' est ''la''-''do''-''mi''-''sol''. Il est composé d'une tierce mineure, d'une quinte juste et d'une septième mineure (c'est un accord parfait mineur auquel on ajoute une septième mineure).
L'accord de septième majeure est l'accord de septième formé sur la fondamentale d'une gamme majeure. Par exemple, L'accord de septième majeure de ''do'' est ''do''-''mi''-''sol''-''si''. Il est composé d'une tierce majeure, d'une quinte juste et d'une septième majeure (c'est un accord parfait majeur auquel on ajoute une septième majeure).
==== Utilisation du chiffrage ====
Le chiffrage est utilisé de deux manières.
La première manière, c'est la notation de la basse continue. La basse continue est une technique d'improvisation utilisée dans le baroque pour l'accompagnement d'instruments solistes. Sur la partition, on indique en général la note de basse de l'accord et le chiffrage en chiffres arabes.
La seconde manière, c'est pour l'analyse d'une partition. Le fait de chiffrer les accords permet de mieux en comprendre la structure.
De manière générale, on peut retenir que :
* le chiffrage « 5 » indique un accord parfait, superposition d'une tierce (majeure ou mineure) et d'une quinte juste ;
* le chiffrage « 6 » indique le premier renversement d'un accord parfait ;
* le chiffrage « 6/4 » indique le second renversement d'un accord parfait ;
* chiffrage « 7/+ » indique un accord de septième de dominante ;
* le signe « + » indique en général que la note de l'intervalle est la sensible ;
* un intervalle barré désigne un intervalle diminué.
[[fichier:Accords gamme do majeur la mineur.svg|class=transparent| center | Principaux accords construits sur les gammes de ''do'' majeur et de ''la'' mineur harmonique.]]
=== Notation « jazz » ===
En jazz et de manière générale en musique rock et populaire, la base d'un accord est la triade composée d'une tierce (majeure ou mineure) et d'une quinte juste. Pour désigner un accord, on utilise la note fondamentale, éventuellement désigné par une lettre dans le système anglo-saxon (A pour ''la'' etc.), suivi d'une qualité (comme « m », « + »…).
Les renversements ne sont pas notés de manière particulière, ils sont notés comme les formes fondamentales.
Dans les deux tableaux suivants, la fondamentale est notée X (remplace le C pour un accord de ''do'', le D pour un accord de ''ré''…). La construction des accords est décrite par la suite.
[[Fichier:Arbre accords triades 5d5J5A.svg|vignette|upright=1.5|Formation des triades présentée sous forme d'arbre.]]
{| class="wikitable"
|+ Notation des principales triades
|-
|
! scope="col" | Tierce<br />mineure (3m)
! scope="col" | Tierce<br />majeure (3M)
|-
! scope="row" | Quinte diminuée (5d)
| X<sup>o</sup>, Xm<sup>♭5</sup>, X–<sup>♭5</sup> ||
|-
! scope="row" | Quinte juste (5J)
| Xm, X– || X
|-
! scope="row" | Quinte augmentée (5A)
| || X+, X<sup>♯5</sup>
|}
[[Fichier:Triades do.svg|class=transparent|center|Triades de do.]]
{| class="wikitable"
|+ Notation des principaux accords de septième
|-
| colspan="2" |
! scope="col" | Tierce<br />mineure (3m)
! scope="col" | Tierce<br />majeure (3M)
|-
! scope="row" rowspan="2" | Quinte<br />diminuée (5d)
! scope="row" | Septième diminuée (7d)
| X<sup>o7</sup> ||
|-
! scope="row" | Septième mineure (7m)
| Xm<sup>7(♭5)</sup>, X–<sup>7(♭5)</sup>, X<sup>Ø</sup> ||
|-
! scope="row" rowspan="3" | Quinte<br />juste (5J)
! scope="row" | Sixte majeure (6M)
| Xm<sup>6</sup> || X<sup>6</sup>
|-
! scope="row" | Septième mineure (7m)
| Xm<sup>7</sup>, X–<sup>7</sup> || X<sup>7</sup>
|-
! scope="row" | Septième majeure (7M)
| Xm<sup>maj7</sup>, X–<sup>maj7</sup>, Xm<sup>Δ</sup>, X–<sup>Δ</sup> || X<sup>maj7</sup>, X<sup>Δ</sup>
|-
! scope="row" rowspan="2" | Quinte<br />augmentée (5A)
! scope="row" | Septième mineure (7m)
| || X+<sup>7</sup>
|-
! scope="row" | Septième majeure (7M)
| || X+<sup>maj7</sup>
|}
[[Fichier:Arbre accords septieme.svg|class=transparent|center|Formation des accords de septième présentée sous forme d'arbre.]]
[[Fichier:Accords do septieme.svg|class=transparent|center|Accord de do septième.]]
On notera que l'intervalle de sixte majeure est l'enharmonique de celui de septième diminuée (6M = 7d).
[[File:Principaux accords do.svg|class=transparent|center|Principaux accords de do.]]
==== Triades ====
; Accords fondés sur une tierce majeure
* accord parfait majeur : pas de notation
*: p. ex. « ''do'' » ou « C » pour l'accord parfait de ''do'' majeur (''do'' - ''mi'' - ''sol'')
; Accords fondés sur une tierce mineure
* accord parfait mineur : « m », « min » ou « – »
*: « ''do'' m », « ''do'' – », « Cm », « C– »… pour l'accord parfait de ''do'' mineur (''do'' - ''mi''♭ - ''sol'')
==== Triades modifiées ====
; Accords fondés sur une tierce majeure
* accord augmenté (la quinte est augmentée) : aug, +, ♯5
*: « ''do'' aug », « ''do'' + », « ''do''<sup>♯5</sup> » « Caug », « C+ » ou « C<sup>♯5</sup> » pour l'accord de ''do'' augmenté (''do'' - ''mi'' - ''sol''♯)
: L'accord augmenté est un empilement de tierces majeures. Ainsi, un accord augmenté a deux notes communes avec deux autres accords augmentés : C+ (''do'' - ''mi'' - ''sol''♯) a deux notes communes avec A♭+ (''la''♭ - ''do'' - ''mi'') et avec E+ (''mi'' - ''sol''♯ - ''si''♯) ; et on remarque que ces trois accords sont en fait enharmoniques (avec les enharmonies ''la''♭ = ''sol''♯ et ''si''♯ = ''do''). En effet, l'octave comporte six tons (sous la forme de cinq tons et deux demi-tons), et une tierce majeure comporte deux tons, on arrive donc à l'octave en ajoutant une tierce majeure à la dernière note de l'accord.
; Accords fondés sur une tierce mineure
* accord diminué (la quinte est diminuée) : dim, o, ♭5
*: « ''do'' dim », « ''do''<sup>o</sup> », « ''do''<sup>♭5</sup> », « Cdim », « C<sup>o</sup> » ou « C<sup>♭5</sup> » pour l'accord de ''do'' diminuné (''do'' - ''mi''♭ - ''sol''♭)
: On remarque que la quinte diminuée est l'enharmonique de la quarte augmentée et est l'intervalle appelé « triton » (car composé de trois tons).
; Accords fondés sur une tierce majeure ou mineure
* accord suspendu de seconde : la tierce est remplacée par une seconde majeure : sus2
*: « ''do''<sup>sus2</sup> » ou « C<sup>sus2</sup> » pour l'accord de ''do'' majeur suspendu de seconde (''do''-''ré''-''sol'')
* accord suspendu de quarte : la tierce est remplacée par une quarte juste : sus4
*: « ''do''<sup>sus4</sup> » ou « C<sup>sus4</sup> » pour l'accord de ''do'' majeur suspendu de quarte (''do''-''fa''-''sol'')
==== Triades appauvries ====
; Accords fondés sur une tierce majeure ou mineure
* accord de puissance : la tierce est omise, l'accord n'est constitué que de la fondamentale et de la quinte juste : 5
*: « ''do''<sup>5</sup> », « C<sup>5</sup> » pour l'accord de puissance de ''do'' (''do'' - ''la'')
{{note|Très utilisé dans les musiques rock, hard rock et heavy metal, il est souvent joué renversé (''la'' - ''do'') ou bien avec l'ajout de l'octave (''do'' - ''la'' - ''do'').}}
==== Triades enrichies ====
; Accords fondés sur une tierce majeure
* accord de septième (la 7<sup>e</sup> est mineure) : 7
*: « ''do''<sup>7</sup> », « C<sup>7</sup> » pour l'accord de ''do'' septième, appelé « accord de septième de dominante de ''fa'' majeur » en musique classique (''do'' - ''mi'' - ''sol'' - ''si''♭)
* accord de septième majeure : Δ, 7M ou maj7
*: « ''do'' <sup>Δ</sup> », « ''do'' <sup>maj7</sup> », « C<sup>Δ</sup> », « C<sup>7M</sup> »… pour l'accord de ''do'' septième majeure (''do'' - ''mi'' - ''sol'' - ''si'')
; Accords fondés sur une tierce mineure
* accord de mineur septième (la tierce et la 7<sup>e</sup> sont mineures) : m7, min7 ou –7
*: « ''do'' m<sup>7</sup> », « ''do'' –<sup>7</sup> », « Cm<sup>7</sup> », « C–<sup>7</sup> »… pour l'accord de ''do'' mineur septième, appelé « accord de septième de dominante de ''fa'' mineur » en musique classique (''do'' - ''mi''♭ - ''sol'' - ''si''♭)
* accord mineure septième majeure : m7M, m7maj, mΔ, –7M, –7maj, –Δ
*: « ''do'' m<sup>7M</sup> », « ''do'' m<sup>maj7</sup> », « ''do'' –<sup>Δ</sup> », « Cm<sup>7M</sup> », « Cm<sup>maj7</sup> », « C–<sup>Δ</sup> »… pour l'accord de ''do'' mineur septième majeure (''do'' - ''mi''♭ - ''sol'' - ''si'')
* accord de septième diminué (la quinte et la septième sont diminuée) : dim 7 ou o7
*: « ''do'' dim<sup>7</sup> », « ''do''<sup>o7</sup> », « Cdim<sup>7</sup> » ou « C<sup>o7</sup> » pour l'accord de ''do'' septième diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si''♭)
* accord demi-diminué (seule la quinte est diminuée, la septième est mineure) : Ø ou –7(♭5)
*: « ''do''<sup>Ø</sup> », « ''do''<sup>7(♭5)</sup> », « C<sup>Ø</sup> » ou « C<sup>7♭5</sup> » pour l'accord de ''do'' demi-diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si'')
=== Construction pythagoricienne des accords ===
Nous avons vu au débuts que lorsque l'on joue deux notes en même temps, leurs vibrations se superposent. Certaines superpositions créent un phénomène de battement désagréable, c'est le cas des secondes.
Dans le cas d'une tierce majeure, les fréquences des notes quadruple et quintuple d'une même base : les fréquences s'écrivent 4׃<sub>0</sub> et 5׃<sub>0</sub>. Cette superposition de vibrations est agréable à l'oreille. Nous avons également vu que dans le cas d'une quinte juste, les fréquences sont le double et le triple d'une même base, ou encore le quadruple et sextuple si l'on considère la moitié de cette base.
Ainsi, dans un accord parfait majeur, les fréquences des fondamentales des notes sont dans un rapport 4, 5, 6. De même, dans le cas d'un accord parfait mineur, les proportions sont de 1/6, 1/5 et 1/4.
{{voir|[[../Caractéristiques_et_notation_des_sons_musicaux#Construction_pythagoricienne_et_gamme_de_sept_tons|Caractéristiques et notation des sons musicaux > Construction pythagoricienne et gamme de sept tons]]}}
=== Un peu de physique : interférences ===
Les sons sont des vibrations. Lorsque l'on émet deux sons ou plus simultanément, les vibrations se superposent, on parle en physique « d'interférences ».
Le modèle le plus simple pour décrire une vibration est la [[w:fr:Fonction sinus|fonction sinus]] : la pression de l'air P varie en fonction du temps ''t'' (en secondes, s), et l'on a pour un son « pur » :
: P(''t'') ≈ sin(2π⋅ƒ⋅''t'')
où ƒ est la fréquence (en hertz, Hz) du son.
Si l'on émet deux sons de fréquence respective ƒ<sub>1</sub> et ƒ<sub>2</sub>, alors la pression vaut :
: P(''t'') ≈ sin(2π⋅ƒ<sub>1</sub>⋅''t'') + sin(2π⋅ƒ<sub>2</sub>⋅''t'').
Nous avons ici une [[w:fr:Identité trigonométrique#Transformation_de_sommes_en_produits,_ou_antilinéarisation|identité trigonométrique]] dite « antilinéarisation » :
: <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{f_1 + f_2}{2}t \right ) \cdot \sin \left ( 2\pi \frac{f_1 - f_2}{2}t \right ).</math>
On peut étudier simplement deux situations simples.
[[Fichier:Battements interferentiels.png|vignette|Deux sons de fréquences proches créent des battements : la superposition d'une fréquence et d'une enveloppe.]]
La première, c'est quand les fréquences ƒ<sub>1</sub> et ƒ<sub>2</sub> sont très proches. Alors, la moyenne (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2 est très proche de ƒ<sub>1</sub> et ƒ<sub>2</sub> ; et la demie différence (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2 est très proche de zéro. On a donc une enveloppe de fréquence très faible, (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2, dans laquelle s'inscrit un son de fréquence moyenne, (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2. C'est cette enveloppe de fréquence très faible qui crée les battements, désagréables à l'oreille.
Sur l'image ci-contre, le premier trait rouge montre un instant où les vibrations sont opposées ; elles s'annulent, le son s'éteint. Le second trait rouge montre un instant où les vibrations sont en phase : elle s'ajoutent, le son est au plus fort.
{{clear}}
La seconde, c'est lorsque les deux fréquences sont des multiples entiers d'une même fréquence fondamentale ƒ<sub>0</sub> : ƒ<sub>1</sub> = ''n''<sub>1</sub>⋅ƒ<sub>0</sub> et ƒ<sub>0</sub> = ''n''<sub>0</sub>⋅ƒ<sub>0</sub>. On a alors :
: <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{n_1 + n_2}{2}f_0 \cdot t \right ) \cdot \sin \left ( 2\pi \frac{n_1 - n_2}{2}f_0 \cdot t \right ).</math>
On multiplie donc deux fonctions qui ont des fréquences multiples de ƒ<sub>0</sub>. La différence minimale entre ''n''<sub>1</sub> et ''n''<sub>2</sub> vaut 1 ; on a donc une enveloppe dont la fréquence est au minimum la moitié de ƒ<sub>0</sub>, c'est-à-dire un son une octave en dessous de ƒ<sub>0</sub>. Donc, cette enveloppe ne crée pas d'effet de battement, ou plutôt, le battement est trop rapide pour être perçu comme tel. Dans cette enveloppe, on a une fonction sinus dont la fréquence est également un multiple de ƒ<sub>0</sub> ; l'enveloppe et la fonction qui y est inscrite ont donc de nombreux « points communs », d'où l'effet harmonieux.
=== Le tonnetz ===
[[File:Speculum musicae.png|thumb|right|225px|Euler, ''De harmoniæ veris principiis'', 1774, p. 350.]]
En allemand, le terme ''Tonnetz'' (se prononce « tône-netz ») signifie « réseau tonal ». C'est une représentation graphique des notes qui a été imaginée par [[w:Leonhard Euler|Leonhard Euler]] en 1739.
Cette représentation graphique peut aider à la mémorisation de certains concepts de l'harmonie. Cependant, son application est très limitée : elle ne concerne que l'intonation juste d'une part, et que les accords parfait des tonalités majeures et mineures naturelles d'autre part. La représentation contenant les douze notes de la musique savante occidentale, on peut bien sûr représenter d'autres objets, comme les accords de septième ou les accords diminués, mais la représentation graphique est alors compliquée et perd son intérêt pédagogique.
On part d'une note, par exemple le ''do''. Si on progresse vers la droite, on monte d'une quinte juste, donc ''sol'' ; vers la gauche, on descend d'une quinte juste, donc ''fa''. Si on va vers le bas, on monte d'une tierce majeure, donc ''mi'' ; si on va vers le haut, on descend d'une tierce majeure, donc ''la''♭ ou ''sol''♯
fa — do — sol — ré
| | | |
la — mi — si — fa♯
| | | |
do♯ — sol♯ — ré♯ — si♭
La figure forme donc un filet, un réseau. On voit que ce réseau « boucle » : si on descend depuis le ''do''♯, on monte d'une tierce majeure, on obtient un ''mi''♯ qui est l'enharmonique du ''fa'' qui est en haut de la colonne. Si on va vers la droite à partir du ''ré'', on obtient le ''la'' qui est au début de la ligne suivante.
Si on ajoute des diagonales allant vers la droite et le haut « / », on met en évidence des tierces mineures : ''la'' - ''do'', ''mi'' - ''sol'', ''si'' - ''ré'', ''do''♯ - ''mi''…
fa — do — sol — ré
| / | / | / |
la — mi — si — fa♯
| / | / | / |
do♯ — sol♯ — ré♯ — si♭
Donc les liens représentent :
* | : tierce majeure ;
* — : quinte juste ;
* / : tierce mineure.
[[Fichier:Tonnetz carre accords fr.svg|thumb|Tonnetz avec les accords parfaits. Les notes sont en notation italienne et les accords en notation jazz.]]
On met ainsi en évidence des triangles dont un côté est une quinte juste, un côté une tierce majeure et un côté une tierce mineure ; c'est-à-dire que les notes aux sommets du triangle forment un accord parfait majeur (par exemple ''do'' - ''mi'' - ''sol'') :
<div style="font-family:courier; background-color:#fafafa">
fa — '''do — sol''' — ré<br />
| / '''| /''' | / |<br />
la — '''mi''' — si — fa♯<br />
| / | / | / |<br />
do♯ — sol♯ — ré♯ — si♭
</div>
ou un accord parfait mineur (''la'' - ''do'' - ''mi'').
<div style="font-family:courier; background-color:#fafafa">
fa — '''do''' — sol — ré<br />
| '''/ |''' / | / |<br />
'''la — mi''' — si — fa♯<br />
| / | / | / |<br />
do♯ — sol♯ — ré♯ — si♭
</div>
Un triangle représente donc un accord, et un sommet représente une note. Si on passe d'un triangle à un triangle voisin, alors on passe d'un accord à un autre accord, les deux accords ayant deux notes en commun. Ceci illustre la notion de « plus court chemin » en harmonie : si on passe d'un accord à un autre en gardant un côté commun, alors on a un mouvement conjoint sur une seule des trois voix.
Par rapport à l'harmonie fonctionnelle : les accords sont contigus à leur fonction, par exemple en ''do'' majeur :
* fonction de tonique ({{Times New Roman|I}}) : C, A– et E– sont contigus ;
* fonction de sous-dominante ({{Times New Roman|IV}}) : F et D– sont contigus ;
* fonction de dominante ({{Times New Roman|V}}) : G et B<sup>o</sup> sont contigus.
On notera que les triangles d'un schéma ''tonnetz'' ne représentent que des accords parfaits. Pour représenter un accord de quinte diminuée (''si'' - ''ré'' - ''fa'') ou les accords de septième, en particulier l'accord de septième de dominante, il faut étendre le ''tonnetz'' et l'on obtient des figures différentes. Par ailleurs, il est adapté à ce que l'on appelle « l'intonation juste », puisque tous les intervalles sont idéaux.
[[Fichier:Tonnetz carre accords etendu fr.svg|vignette|Tonnetz étendu.]]
[[Fichier:Tonnetz carre do majeur accords fr.svg|vignette|Tonnetz de la tonalité de ''do'' majeur. La représentation de l'accord de quinte diminuée sur ''si'' (B<sup>o</sup>) est une ligne et non un triangle.]]
[[Fichier:Tonnetz carre do mineur accords fr.svg|vignette|Tonnetz des tonalités de ''do'' mineur naturel (haut) et ''do'' mineur harmonique (bas).]]
Si l'on étend un peu le réseau :
ré♭ — la♭ — mi♭ — si♭ — fa
| / | / | / | / |
fa — do — sol — ré — la
| / | / | / | / |
la — mi — si — fa♯ — do♯
| / | / | / | / |
do♯ — sol♯ — ré♯ — la♯ — mi♯
| / | / | / | / |
mi♯ — do — sol — ré — la
on peut donc trouver des chemins permettant de représenter les accords de septième de dominante (par exemple en ''do'' majeur, G<sup>7</sup>)
fa
/
sol — ré
| /
si
et des accords de quinte diminuée (en ''do'' majeur : B<sup>o</sup>)
fa
/
ré
/
si
Une gamme majeure ou mineure naturelle peut se représenter par un trapèze rectangle : ''do'' majeur
fa — do — sol — ré
| /
la — mi — si
et ''do'' mineur
la♭ — mi♭ — si♭
/ |
fa — do — sol — ré
En revanche, la représentation d'une tonalité nécessite d'étendre le réseau afin de pouvoir faire figurer tous les accords, deux notes sont représentées deux fois. La représentation des tonalités mineures harmoniques prend une forme biscornue, ce qui nuit à l'intérêt pédagogique de la représentation.
[[Fichier:Neo-Riemannian Tonnetz.svg|vignette|upright=2|Tonnetz avec des triangles équilatéraux.]]
On peut réorganiser le schéma en décalant les lignes, afin d'avoir des triangles équilatéraux. Sur la figure ci-contre (en notation anglo-saxonne) :
* si on monte en allant vers la droite « / », on a une tierce mineure ;
* si on descend en allant vers la droite « \ », on a une tierce majeure ;
* les liens horizontaux « — » représentent toujours des quintes justes
* les triangles pointe en haut sont des accords parfaits mineurs ;
* les triangles pointe en bas sont des accords parfaits majeurs.
On a alors les accords de septième de dominante
F
/
G — D
\ /
B
et de quinte diminuée
F
/
D
/
B
les tonalités majeures
F — C — G — D
\ /
A — E — B
et les tonalités mineures naturelles
A♭ — E♭ — B♭
/ \
F — C — G — D
== Notes et références ==
{{références}}
== Voir aussi ==
=== Liens externes ===
{{wikipédia|Consonance (harmonie tonale)}}
{{wikipédia|Disposition de l'accord}}
{{wikisource|Petit Manuel d’harmonie}}
* {{lien web
| url = https://www.apprendrelesolfege.com/chiffrage-d-accords
| titre = Chiffrage d'accords (classique)
| site = Apprendrelesolfege.com
| consulté le = 2020-12-03
}}
* {{lien web
| url = https://www.coursd-harmonie.fr/introduction/introduction2_le_chiffrage_d_accords.php
| titre = Introduction II : Le chiffrage d'accords
| site = Cours d'harmonie.fr
| consulté le = 2021-12-14
}}
* {{lien web
| url=https://www.coursd-harmonie.fr/
| titre = Cours d'harmonie en ligne
| auteur = Jean-Baptiste Voinet
| site=coursd-harmonie.fr
| consulté le = 2021-12-20
}}
* {{lien web
| url=http://e-harmonie.e-monsite.com/
| titre = Cours d'harmonie classique en ligne
| auteur = Olivier Miquel
| site=e-harmonie
| consulté le = 2021-12-24
}}
* {{lien web
| url=https://fr.audiofanzine.com/theorie-musicale/editorial/dossiers/les-gammes-et-les-modes.html
| titre = Les bases de l’harmonie
| site = AudioFanzine
| date = 2013-07-23
| consulté le = 2024-01-12
}}
----
''[[../Mélodie|Mélodie]]'' < [[../|↑]] > ''[[../Représentation musicale|Représentation musicale]]''
[[Catégorie:Formation musicale (livre)|Harmonie]]
ccfpyu8uf9c5ktdw9e8e6n7pp0jo91g
745866
745865
2025-07-03T11:42:11Z
Cdang
1202
/* Harmonisation par des accords de septième */ synoptique
745866
wikitext
text/x-wiki
{{Bases de solfège}}
<span style="font-size:25px;">6. Harmonie</span>
L'harmonie désigne les notes jouées en même temps, soit plusieurs instruments jouant chacun une note, soit un instrument jouant un accord (instrument dit polyphonique).
== Première approche ==
L'exemple le plus simple d'harmonie est sans doute la chanson en canon : c'est un chant polyphonique, c'est-à-dire à plusieurs voix, chaque voix chantant la même chose en décalé. Prenons par exemple ''Vent frais, vent du matin'' (la version originale est ''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609) :
[[Fichier:Vent frais vent du matin.svg|class=transparent|center|Partition de ''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]]
[[Fichier:Vent frais vent du matin.midi|vignette|''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]]
nous voyons que les voix se superposent de manière « harmonieuse ». Les notes de chaque voix se correspondent point par point (avec un retard), c'est donc un type d'harmonie polyphonique appelé « contrepoint ».
Considérons la première note de la mesure 6 pour chaque voix. Nous avons la superposition des notes ''ré''-''fa''-''la'' (du grave vers l'aigu) ; la superposition de notes jouées ou chantées ensembles s'appelle un accord. Cet accord ''ré''-''fa''-''la'' porte le nom « d'accord parfait de ''ré'' mineur » :
* « ''ré'' » car la note fondamentale est un ''ré'' ;
* « parfait » car il est l'association d'une tierce, ''ré''-''fa'', et d'une quinte juste, ''ré''-''la'' ;
* « mineur » car le premier intervalle, ''ré''-''fa'', est une tierce mineure.
Considérons maintenant un chant accompagné au piano. La piano peut jouer plusieurs notes en même temps, il peut jouer des accords.
[[Fichier:Au clair de le lune chant et piano.svg|class=transparent|center|Deux premières mesure d’Au clair de la lune.]]
[[Fichier:Au clair de le lune chant et piano.midi|vignette|Deux premières mesure d’Au clair de la lune.]]
L'accord, les notes à jouer simultanément, sont écrites « en colonne ». Lorsqu'on les énonce, on les lit de bas en haut mais le pianiste les joue en pressant les touches du clavier en même temps, de manière « plaquée ».
Le premier accord est composé des notes ''do''-''mi''-''sol'' ; il est appelé « accord parfait de ''do'' majeur » car la note fondamentale est ''do'', qu'il est l'association d'une tierce et d'une quinte juste et que le premier intervalle, ''do''-''mi'', est une tierce majeure.
== Consonance et dissonance ==
Les notions de consonance et de dissonance sont culturelles et changent selon l'époque. Nous pouvons néanmoins noter que :
* l'accord de seconde, et son renversement la septième, créent des battements, les notes « frottent », c'est un intervalle harmonique dissonant ; mais dans le cas de la septième, comme les notes sont éloignées, le frottement est moins perceptible ;
* les accords de tierce, quarte et quinte sonnent agréablement à l'oreille, ils sont consonants.
Dans la musique savante européenne, au début au du Moyen-Âge, seuls les accords de quarte et de quinte étaient considérés comme consonants, d'où leur qualification de « juste ». La tierce, et son renversement la sixte, étaient perçues comme dissonantes.
L'harmonie joue avec les consonances et les dissonances. Dans un premier temps, les harmonies dissonantes sont utilisées pour créer des tensions qui sont ensuite résolues, on utilise des successions « consonant-dissonant-consonant ». À force d'entendre des intervalles considérés comme dissonants, l'oreille s'habitue et certains finissent par être considérés comme consonants ; c'est ce qui est arrivé à la tierce et à la sixte à la fin du Moyen Âge avec le contrepoint.
Il faut ici aborder la notion d'harmonique des notes.
[[File:Harmoniques de do.svg|thumb|Les six premières harmoniques de ''do''.]]
Lorsque l'on joue une note, on entend d'autres notes plus aigües et plus faibles ; la note jouée est appelée la « fondamentale » et les notes plus aigües et plus faibles sont les « harmoniques ». C'est cette accumulation d'harmoniques qui donne la couleur au son, son timbre, qui fait qu'un piano ne sonne pas comme un violon. Par exemple, si l'on joue un ''do''<sup>1</sup><ref>Pour la notation des octaves, voir ''[[../Représentation_musicale#Désignation_des_octaves|Représentation musicale > Désignation des octaves]]''.</ref> (fondamentale), on entend le ''do''<sup>2</sup> (une octave plus aigu), puis un ''sol''<sup>2</sup>, puis encore un ''do''<sup>3</sup> plus aigu, puis un ''mi''<sup>3</sup>, puis encore un ''sol''<sup>3</sup>, puis un ''si''♭<sup>3</sup>…
Ainsi, puisque lorsque l'on joue un ''do'' on entend aussi un ''sol'' très léger, alors jouer un ''do'' et un ''sol'' simultanément n'est pas choquant. De même pour ''do'' et ''mi''. De là vient la notion de consonance.
Le statut du ''si''♭ est plus ambigu. Il fait partie des harmoniques qui sonnent naturellement, mais il forme une seconde descendante avec le ''do'', intervalle dissonant. Par ailleurs, on remarque que le ''si''♭ ne fait pas partie de la gamme de ''do'' majeur, contrairement au ''sol'' et au ''mi''.
Pour le jeu sur les dissonances, on peut écouter par exemple la ''Toccata'' en ''ré'' mineur, op. 11 de Sergueï Prokofiev (1912).
: {{lien web |url=https://www.youtube.com/watch?v=AVpnr8dI_50 |titre=Yuja Wang Prokofiev Toccata |site=YouTube |date=2019-02-26 |consulté le=2021-12-19}}
== Contrepoint ==
Dans le chant grégorien, la notion d'accord n'existe pas. L'harmonie provient de la superposition de plusieurs mélodies, notamment dans ce que l'on appelle le « contrepoint ».
Le terme provient du latin ''« punctum contra punctum »'', littéralement « point par point », et désigne le fait que les notes de chaque voix se correspondent.
L'exemple le plus connu de contrepoint est le canon, comme par exemple ''Frère Jacques'' : chaque note d'un couplet correspond à une note du couplet précédent.
Certains morceaux sont bâtis sur une écriture « en miroir » : l'ordre des notes est inversé entre les deux voix, ou bien les intervalles sont inversés (« mouvement contraire » : une tierce montante sur une voix correspond à une tierce descendante sur l'autre).
On peut également citer le « mouvement oblique » (une des voix, le bourdon, chante toujours la même note) et le mouvement parallèle (les deux voix chantent le même air mais transposé, l'une est plus aiguë que l'autre).
Nous reproduisons ci-dessous le début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.
[[Fichier:Haendel Sonate en trio re mineur debut canon.svg | vignette | center | upright=2 | Début du second ''Allergo'' de la sonate en trio en ''ré'' mineur de Haendel.]]
[[Fichier:Haendel Sonate en trio re mineur debut.midi | vignette | Début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.]]
Nous avons mis en évidence la construction en canon avec des encadrés de couleur : sur les quatre premières mesures, nous voyons trois thèmes repris alternativement par une voix et par l'autre. Ce type de procédé est très courant dans la musique baroque.
Les procédés du contrepoint s'appliquent également à la danse :
* unisson : les danseurs et danseuses font les mêmes gestes en même temps ;
* répétition : le fait de répéter une série de gestes, une « phrase dansante » ;
* canon : les gestes sont faits avec un décalage régulier d'un danseur ou d'une danseuse à l'autre ;
* cascade : forme de canon dans laquelle le décalage est très petit ;
* contraste : deux danseur·euses, ou deux groupes, ont des gestuelles très différentes ;
* accumulation : la gestuelle se complexifie par l'ajout d'éléments au fur et à mesure ; ou bien le nombre de danseur·euses augmente ;
* dialogue : les gestes de danseur·euses ou de groupes se répondent ;
* contre-point : la gestuelle d'un ou une danseuse se superpose à la gestuelle d'un groupe ;
* lâcher-rattraper : les danseurs et danseuses alternent danse à l'unisson et gestuelles indépendantes.
: {{lien web
| url=https://www.youtube.com/watch?v=wgblAOzedFc
| titre=Les procédés de composition en danse
| auteur= Doisneau Sport TV
| site=YouTube
| date=2020-03-16 | consulté le=2021-01-21
}}
{{...}}
== Les accords en général ==
Initialement, on a des chants polyphoniques, des voix qui chantent chacune une mélodie, les mélodies se mêlant. On remarque que certaines superpositions de notes sonnent de manière plus ou moins agréables, consonantes ou dissonantes. On en vient alors à associer ces notes, c'est-à-dire à considérer dès le départ la superposition de ces notes et non pas la rencontre de ces notes au gré des mélodies. Ces groupes de notes superposées forment les accords. En Europe, cette notion apparaît vers le {{pc|xiv}}<sup>e</sup> siècle avec notamment la ''[[wikipedia:fr:Messe de Notre Dame|Messe de Notre Dame]]'' de Guillaume de Machaut (vers 1360-1365). La notion « d'accord parfait » est consacrée par [[wikipedia:fr:Jean-Philippe Rameau|Jean-Philippe Rameau]] dans son ''Traité de l'harmonie réduite à ses principes naturels'', publié en 1722.
=== Qu'est-ce qu'un accord ? ===
Un accord est un ensemble d'au minimum trois notes jouées en même temps. « Jouées » signifie qu'il faut qu'à un moment donné, elles sonnent en même temps, mais le début ou la fin des notes peut être à des instants différents.
Considérons que l'on joue les notes ''do'', ''mi'' et ''sol'' en même temps. Cet accord s'appelle « accord de ''do'' majeur ». En musique classique, on lui adjoint l'adjectif « parfait » : « accord parfait de ''do'' majeur ».
Nous représentons ci-dessous trois manière de faire l'accord : avec trois instruments jouant chacun une note :
[[Fichier:Do majeur trois portees.svg|class=transparent|center|Accord de ''do'' majeur avec trois instruments différents.]]
Avec un seul instrument jouant simultanément les trois notes :
[[Fichier:Chord C.svg|class=transparent|center|Accord de ''do'' majeur joué par un seul instrument.]]
L'accord tel qu'il est joué habituellement par une guitare d'accompagnement :
[[Fichier:Do majeur guitare.svg|class=transparent|center|Accord de ''do'' majeur à la guitare.]]
Pour ce dernier, nous représentons le diagramme indiquant la position des doigts sur le manche au dessus de la portée et la tablature en dessous. Ici, c'est au total six notes qui sont jouées : ''mi'' grave, ''do'' médium, ''mi'' médium, ''sol'' médium, ''do'' aigu, ''mi'' aigu. Mais il s'agit bien des trois notes ''do'', ''mi'' et ''sol'' jouées à des octaves différentes. Nous remarquons également que la note de basse (la note la plus grave), ''mi'', est différente de la note fondamentale (celle qui donne le nom à l'accord), ''do'' ; l'accord est dit « renversé » (voir plus loin).
=== Comment joue-t-on un accord ? ===
Les notes ne sont pas forcément jouées en même temps ; elles peuvent être « égrainées », jouée successivement, ce que l'on appelle un arpège. La partition ci-dessous montre six manières différentes de jouer un accord de ''la'' mineur à la guitare, plaqué puis arpégé.
[[Fichier:La mineur differentes executions.svg|class=transparent|center|Différentes exécution de l'accord de do majeur à la guitare.]]
[[Fichier:La mineur differentes executions midi.midi|vignette|Différentes exécution de l'accord de la mineur à la guitare.]]
Vous pouvez écouter l'exécution de cette partition avec le lecteur ci-contre.
Seuls les instruments polyphoniques peuvent jouer les accords plaqués : instruments à clavier (clavecin, orgue, piano, accordéon), les instruments à plusieurs cordes pincées (harpe, guitare ; violon, alto, violoncelle et contrebasse joués en pizzicati). Les instruments à corde frottés de la famille du violon peuvent jouer des notes par deux à l'archet mais pas plus du fait de la forme bombée du chevalet ; cependant, un mouvement rapide permet de jouer les quatre cordes de manière très rapprochée. Les instruments à percussion de type xylophone ou le tympanon permettent de jouer jusqu'à quatre notes simultanément en tenant deux baguettes (mailloches, maillets) par main.
Tous les instruments peuvent jouer des arpèges même si, dans le cas des instruments monodiques, les notes ne continuent pas à sonner lorsque l'on passe à la note suivante.
L'arpège peut être joué par l'instrument de basse (basson, violoncelle, contrebasse, guitare basse, pédalier de l'orgue…), notamment dans le cas d'une basse continue ou d'une ''{{lang|en|walking bass}}'' (« basse marchante » : la basse joue des noires, donnant ainsi l'impression qu'elle marche).
En jazz, et spécifiquement au piano, on a recours au ''{{lang|en|voicing}}'' : on choisit la manière dont on organise les notes pour donner une couleur spécifique, ou bien pour créer une mélodie en enchaînant les accords. Il est fréquent de ne pas jouer toutes les notes : si on n'en garde que deux, ce sont la tierce et la septième, car ce sont celles qui caractérisent l'accord (selon que la tierce est mineure ou majeure, que la septième est majeure ou mineure), et la fondamentale est en général jouée par la contrebasse ou guitare basse.
{{clear}}
=== Classes d'accord ===
[[Fichier:Intervalles harmoniques accords classes.svg|vignette|upright=1.5|Intervalles harmoniques dans les accords classés de trois, quatre et cinq notes.]]
Un accord composé d'empilement de tierces est appelé « accord classé ». En musique tonale, c'est-à-dire la musique fondée sur les gammes majeures ou mineures (cas majoritaire en musique classique), on distingue trois classes d'accords :
* les accords de trois notes, ou triades, ou accords de quinte ;
* les accords de quatre notes, ou accords de septième ;
* les accords de cinq notes, ou accords de neuvième.
En empilant des tierces, si l'on part de la note fondamentale, on a donc de intervalles de tierce, quinte, septième et neuvième.
En musique tonale, les accords avec d'autres intervalles (hors renversement, voir ci-après), typiquement seconde, quarte ou sixte, sont considérés comme des transitions entre deux accords classés. Ils sont appelés, selon leur utilisation, « accords à retard » (en anglais : ''{{lang|en|suspended chord}}'', accord suspendu) ou « appoggiature » (note « appuyée », étrangère à l'harmonie). Voir aussi plus loin la notion de note étrangère.
=== Renversements d'accords ===
[[File:Accord do majeur renversements.svg|thumb|Accord parfait de do majeur et ses renversements.]]
[[Fichier:Progression dominante renverse parfait do majeur.svg|vignette|upright=0.6|Progression accord de dominante renversé → accord parfait en ''do'' majeur.]]
Un accord classé est donc un empilement de tierces. Si l'on change l'ordre des notes, on a toujours le même accord mais il est fait avec d'autres intervalles harmoniques. Par exemple, l'accord parfait de ''do'' majeur dans son état fondamental, c'est-à-dire non renversé, s'écrit ''do'' - ''mi'' - ''sol''. Sa note fondamentale, ''do'', est aussi se note de basse.
Si maintenant on prend le ''do'' de l'octave supérieure, l'accord devient ''mi - sol - do'' ; c'est l'empilement d'une tierce ''(mi - sol)'' et d'une quarte ''(sol - do)'', soit la superposition d'une tierce ''(mi - sol)'' et d'une sixième ''(mi - do)''. C'est le premier renversement de l'accord parfait de ''do'' majeur ; la fondamentale est toujours ''do'' mais la basse est ''mi''. Le second renversement est ''sol - do - mi''.
L'utilisation de renversement peut faciliter l'exécution de la progression d'accord. Par exemple, en tonalité ''do'' majeur, si l'on veut passer de l'accord de dominante ''sol - si - ré'' à l'accord parfait ''do - mi - sol'', alors on peut utiliser le second renversement de l'accord de dominante : ''ré - sol - si'' → ''do - mi - sol''. Ainsi, la basse descend juste d'un ton ''(ré → do)'' et sur un piano, la main reste globalement dans la même position.
Le renversement d'un accord permet également de respecter certaines règles de l'harmonie classique, notamment éviter que des voix se suivent strictement (« mouvement parallèle »), ce qui aurait un effet de platitude.
De manière générale, la notion de renversement permet deux choses :
* d'enrichir l'œuvre : pour créer une harmonie donnée (c'est-à-dire des sons sonnant bien ensemble), nous avons plus de souplesse, nous pouvons organiser ces notes comme nous le voulons selon les voix ;
* de simplifier l'analyse : quelle que soit la manière dont sont organisées les notes, cela nous ramène à un même accord.
{{citation bloc|Or il, y a plusieurs manières de jouer un accord, selon que l'on aborde par la première note qui le constitue, ''do mi sol'', la deuxième, ''mi sol do'', ou la troisième note, ''sol do mi''. Ce sont les renversements, [que Rameau] va classer en différentes combinaisons d'une seule matrice. Faisant cela, Rameau divise le nombre d'accords [de septième] par quatre. Il simplifie, il structure […].|{{ouvrage|prénom1=André |nom1=Manoukian |titre=Sur les routes de la musique |éditeur=Harper Collins |année=2021 |passage=54 |isbn=979-1-03391201-9}} }}
{{clear}}
[[File:Plusieurs realisation 1er renversement doM.svg|thumb|Plusieurs réalisation du premier renversement de l'accord de ''do'' majeur.]]
Notez que dans la notion de renversement, seule importe en fait la note de basse. Ainsi, les accords ''mi-sol-do'', ''mi-do-sol'', ''mi-do-mi-sol'', ''mi-sol-mi-do''… sont tous une déclinaison du premier renversement de ''do-mi-sol'' et ils seront abrégés de la même manière (''mi''<sup>6</sup> en musique classique ou C/E en musique populaire et jazz, voir plus bas).
{{clear}}
== Notation des accords de trois notes ==
Les accords de trois notes sont appelés « accords de quinte » en classique, et « triades » en jazz.
[[Fichier:Progression dominante renverse parfait do majeur chiffrage.svg|vignette|upright=0.7|Chiffrage du second renversement d'un accord de ''sol'' majeur et d'un accord de ''do'' majeur : notation en musique populaire et jazz (haut) et notation de basse chiffrée (bas).]]
Les accords sont construits de manière systématique. Nous pouvons donc les représenter de manière simplifiée. Cette notation simplifiée des accords est appelée « chiffrage ».
Reprenons la progression d'accords ci-dessus : « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » dans la tonalité de ''do'' majeur. On utilise en général trois notations différentes :
* en musique populaire, jazz, rock… un accord est désigné par sa note fondamentale ; ici donc, les accords sont notés « ''sol'' - ''do'' » ou, en notation anglo-saxonne, « G - C » ;<br /> comme le premier accord est renversé, on indique la note de basse après une barre, la progression d'accords est donc chiffrée '''« ''sol''/''ré'' - ''do'' »''' ou '''« G/D - C »''' ;<br /> il s'agit ici d'accords composés d'une tierce majeure et d'une quinte juste ; si les accords sont constitués d'intervalles différents, nous ajoutons un symbole après la note : « m » ou « – » si la tierce est mineure, « dim » ou « ° » si la quinte est diminuée ;
* en musique classique, on utilise la notation de « basse chiffrée » (utilisée notamment pour noter la basse continue en musique baroque) : on indique la note de basse sur la portée et on lui adjoint l'intervalle de la fondamentale à la note la plus haute (donc ici respectivement 6 et 5, puisque ''sol''-''si'' est une sixte et ''do''-''sol'' est une quinte), étant sous-entendu que l'on a des empilements de tierce en dessous ; mais dans le cas du premier accord, le premier intervalle n'est pas une tierce, mais une quarte ''(ré''-''sol)'', on note donc '''« ''ré'' <sup>6</sup><sub>4</sub> - ''do'' <sup>5</sup> »'''<ref>quand on ne dispose pas de la notation en supérieur (exposant) et inférieur (indice), on utilise parfois une notation sous forme de fraction : ''sol'' 6/4 et ''do'' 5/.</ref> ;
* lorsque l'on fait l'analyse d'un morceau, on s'attache à identifier la note fondamentale de l'accord (qui est différente de la basse dans le cas d'un renversement) ; on indique alors le degré de la fondamentale : '''« {{Times New Roman|V<sup>6</sup><sub>4</sub> - I<sup>5</sup>}} »'''.
La notation de basse chiffrée permet de construire l'accord à la volée :
* on joue la note indiquée (basse) ;
* s'il n'y a pas de 2 ni de 4, on lui ajoute la tierce ;
* on ajoute les intervalles indiqués par le chiffrage.
La notation de musique jazz oblige à connaître la composition des différents accords, mais une fois que ceux-ci sont acquis, il n'y a pas besoin de reconstruire l'accord.
La notation de basse chiffrée avec les chiffres romains n'est pas utilisée pour jouer, mais uniquement pour analyser ; Sur les partitions avec basse chiffrée, il y a simplement les chiffrages indiqués au-dessus de la partie de basse. Le chiffrage avec le degré en chiffres romains présente l'avantage d'être indépendant de la tonalité et donc de se concentrer sur la fonction de l'accord au sein de la tonalité. Par exemple, ci-dessous, nous pouvons parler de la progression d'accords « {{Times New Roman|V - I}} » de manière générale, cette notation étant valable quelle que soit la tonalité.
[[File:Progression dominante renverse parfait do majeur chiffrage basse continue.svg|thumb|Chiffrage en notation basse chiffrée de la progression d'accords « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » en do majeur.]]
{{note|En notation de base continue avec fondamentale en chiffres romains, la fondamentale est toujours indiquée ''sous'' la portée de la partie de basse. Les intervalles sont indiqués au-dessus de la portée de la partie de basse ; lorsque l'on fait une analyse, on peut ayssi les indiquer à côté du degré en chiffres romains, donc sous la portée de la basse.}}
{{note|En notation rock, le 5 en exposant indique un accord incomplet avec uniquement la fondamentale et la quinte, un accord sans tierce appelé « accord de puissance » ou ''{{lang|en|power chord}}''. Par exemple, C<sup>5</sup> est l'accord ''do-sol''.}}
{{clear}}
[[Fichier:Accords parfait do majeur basse chiffree fondamental et renverse.svg|vignette|upright=2.5|Chiffrage de l'accord parfait de ''do'' majeur en basse chiffrée, à l'état fondamental et ses renversements.]]
Concernant les accords parfaits en notation de basse chiffrée :
* un accord parfait à l'état fondamental est chiffré « <sup>5</sup> » ; on l'appelle « accord de quinte » ;
* le premier renversement est chiffré « <sup>6</sup> » (la tierce est implicite) ; on l'appelle « accord de sixte » ;
* le second renversement est noté « <sup>6</sup><sub>4</sub> » ; on l'appelle « accord de sixte et de quarte » (ou bien « de quarte et de sixte »).
Par exemple, pour l'accord parfait de ''do'' majeur :
* l'état fondamental ''do''-''mi''-''sol'' est noté ''do''<sup>5</sup> ;
* le premier renversement ''mi''-''sol''-''do'' est noté ''mi''<sup>6</sup> ;
* le second renversement ''sol''-''do''-''mi'' est noté ''sol''<sup>6</sup><sub>4</sub>.
Il y a une exception : l'accord construit sur la sensible (7{{e}} degré) contient une quinte diminuée et non une quinte juste. Le chiffrage est donc différent :
* l'état fondamental ''si''-''ré''-''fa'' est noté ''si''<sup><s>5</s></sup> (cinq barré), « accord de quinte diminuée » ;
* le premier renversement ''ré''-''fa''-''si'' est noté ''ré''<sup>+6</sup><sub>3</sub>, « accord de sixte sensible et tierce » ;
* le second renversement ''fa''-''si''-''ré'' est noté ''fa''<sup>6</sup><sub>+4</sub>, « accord de sixte et quarte sensible ».
Par ailleurs, on ne considère pas qu'il est fondé sur la sensible, mais sur la dominante ; on met donc des guillemets autour du degré, « “V” ». Donc selon l'état, le chiffrage est “V”<sup><s>5</s></sup>, “V”<sup>+6</sup><sub>3</sub> ou “V”<sup>6</sup><sub>+4</sub>.
En notation jazz, on ajoute « dim », « <sup>o</sup> » ou bien « <sup>♭5</sup> » au chiffrage, ici : B dim, B<sup>o</sup> ou B<sup>♭5</sup> pour l'état fondamental. Pour les renversements : B dim/D et B dim/F ; ou bien B<sup>o</sup>/D et B<sup>o</sup>/F ; ou bien B<sup>♭5</sup>/D et B<sup>♭5</sup>/F.
{{clear}}
[[Fichier:Accords basse chiffree basse do fondamental et renverses.svg|vignette|upright=2|Basse chiffrée : accords de quinte, de sixte et de sixte et de quarte ayant pour basse ''do''.]]
Et concernant les accords ayant pour basse ''do'' en tonalité de ''do'' majeur :
* l'accord ''do''<sup>5</sup> est un accord à l'état fondamental, c'est donc l'accord ''do''-''mi''-''sol'' (sa fondamentale est ''do'') ;
* l'accord ''do''<sup>6</sup> est le premier renversement d'un accord, c'est donc l'accord ''do''-''mi''-''la'' (sa fondamentale est ''la'') ;
* l'accord ''do''<sup>6</sup><sub>4</sub> est le second renversement d'un accord, c'est donc l'accord ''do''-''fa''-''la'' (sa fondamentale est ''fa'').
{{clear}}
== Notes étrangères ==
La musique européenne s'appuie essentiellement sur des accords parfaits, c'est-à-dire fondés sur une tierce majeure ou mineure, et une quinte juste. Il arrive fréquemment qu'un accord ne soit pas un accord parfait. Les notes qui font partie de l'accord parfait sont appelées « notes naturelles » et la note qui n'en fait pas partie est appelée « note étrangère ».
Il existe plusieurs types de notes étrangères :
* anticipation : la note étrangère est une note naturelle de l'accord suivant ;
* appogiature : note d'ornementation qui se résout par mouvement conjoint, c'est-à-dire qu'elle est suivie par une note située juste au-dessus ou en dessous (seconde ascendante ou descendante) qui est, elle, une note naturelle ;
* broderie : on part d'une note naturelle, on monte ou on descend d'une seconde, puis on revient sur la note naturelle ;
* double broderie : on part d'une note naturelle, on joue la note du dessus puis la note du dessous avant de revenir à la note naturelle ; ou bien on joue la note du dessous puis la note du dessus ;
* échappée : note étrangère n'appartenant à aucune des autres catégories ;
* note de passage : mouvement conjoint allant d'une note naturelle d'un accord à une note naturelle de l'accord suivant ;
* pédale : la note de basse reste la même pendant plusieurs accords successifs ;
* retard : la note étrangère est une note naturelle de l'accord précédent.
Les notes étrangères ne sont pas chiffrées.
[[File:Notes etrangeres accords.svg|center|Différents types de notes étrangères.]]
{{note|Les anglophones distinguent deux types de retard : la ''{{lang|en|suspension}}'' est résolue vers le haut (le mouvement est ascendant), le ''{{lang|en|retardation}}'' est résolu vers le bas (le mouvement est descendant).}}
== Principaux accords ==
Les trois principaux accords sont :
* l'accord parfait majeur : il est construit sur les degrés {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (médiante) et {{Times New Roman|V}} (dominante) d'une gamme majeure ; il est noté {{Times New Roman|I}}<sup>5</sup>, {{Times New Roman|IV}}<sup>5</sup>, {{Times New Roman|V}}<sup>5</sup> ;
* l'accord parfait mineur : il est construit sur les degrés {{Times New Roman|I}} (tonique) et {{Times New Roman|IV}} (sous-tonique) d'une gamme mineure harmonique ; il est également noté {{Times New Roman|I}}<sup>5</sup> et {{Times New Roman|IV}}<sup>5</sup>, les anglo-saxons le notent {{Times New Roman|i}}<sup>5</sup> et {{Times New Roman|iv}}<sup>5</sup> (la minuscule indiquant le caractère mineur) ;
* l'accord de septième de dominante : il est construit sur le degré {{Times New Roman|V}} (dominante) d'une gamme majeure ou mineure harmonique ; il est noté {{Times New Roman|V}}<sup>7</sup><sub>+</sub>.
On peut trouver ces trois accords sur d'autres degrés, et il existe d'autre types d'accords. Nous verrons cela plus loin.
{| class="wikitable"
|+ Constitution des principaux accords — Dénomination classique
|-
! scope="col" | Accord
! scope="col" | 1<sup>er</sup> intervalle
! scope="col" | 2<sup>e</sup> intervalle
! scope="col" | 3<sup>e</sup> intervalle
|-
! scope="row" | Accord parfait majeur
| tierce majeure (3M) || quinte juste (5J) || —
|-
! scope="row" | Accord parfait mineur
| tierce mineure (3m) || quinte juste (5J) || —
|-
! scope="row" | Accord de septième de dominante
| tierce majeure (3M) || quinte juste (5J) || septième mineure (7m)
|}
{| class="wikitable"
|+ Constitution des principaux accords — Dénomination jazz
|-
! scope="col" | Accord
! scope="col" | 1<sup>er</sup> intervalle
! scope="col" | 2<sup>e</sup> intervalle
! scope="col" | 3<sup>e</sup> intervalle
|-
! scope="row" | Triade majeure
| tierce majeure (3M) || quinte juste (5J) || —
|-
! scope="row" | Triade mineure
| tierce mineure (3m) || quinte juste (5J) || —
|-
! scope="row" | Accord de septième
| tierce majeure (3M) || quinte juste (5J) || septième mineure (7m)
|}
{| border="0"
|-
| [[Fichier:Accord do majeur arpege puis plaque.midi | Accord parfait de ''do'' majeur (C).]] || [[Fichier:Accord do mineur arpege puis plaque.midi | Accord parfait de ''do'' mineur (Cm).]] || [[Fichier:Accord do septieme arpege puis plaque.midi | Accord de septième de dominante de ''fa'' majeur (C<sup>7</sup>).]]
|-
| Accord parfait<br /> de ''do'' majeur (C). || Accord parfait<br /> de ''do'' mineur (Cm). || Accord de septième de dominante<br /> de ''fa'' majeur (C<sup>7</sup>).
|}
'''Rappel :'''
* la tierce mineure est composée d'un ton et demi (1 t ½) ;
* la tierce majeur est composée de deux tons (2 t) ;
* la quinte juste a la même altération que la fondamentale, sauf lorsque la fondamentale est ''si'' (la quinte juste est alors ''fa''♯) ;
* la septième mineure est le renversement de la seconde majeure (1 t).
[[File:Renversements accords pft fa maj basse chiffree.svg|thumb|Renversements de l'accord parfait de ''fa'' majeur, et la notation de basse chiffrée.]]
[[File:Renversements accord sept de dom fa maj basse chiffree.svg|thumb|Renversements de l'accord de septième de dominante de ''fa'' majeur, et la notation de basse chiffrée.]]
{| class="wikitable"
|+ Notation des principaux accords en musique classique
|-
! scope="col" | Accord
! scope="col" | État<br /> fondamental
! scope="col" | Premier<br /> renversement
! scope="col" | Deuxième<br /> renversement
! scope="col" | Troisième<br /> renversement
|-
! scope="row" | Accord parfait
| {{Times New Roman|I<sup>5</sup>}}<br/> acc. de quinte || {{Times New Roman|I<sup>6</sup>}}<br :> acc. de sixte || {{Times New Roman|I<sup>6</sup><sub>4</sub>}}<br /> acc. de quarte et de sixte || —
|-
! scope="row" | Accord de septième<br /> de dominante
| {{Times New Roman|V<sup>7</sup><sub>+</sub>}}<br /> acc.de septième de dominante || {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}<br />acc. de sixte et quinte diminuée || {{Times New Roman|V<sup>+6</sup>}}<br />acc. de sixte sensible || {{Times New Roman|V<sup>+4</sup>}}<br />acc. de quarte sensible<br />acc. de triton
|}
{| class="wikitable"
|+ Notation des principaux accords en jazz
|-
! scope="col" | Accord
! scope="col" | Chiffrage
! scope="col" | Renversements
|-
! scope="row" | Triade majeure
| X
| rowspan="3" | Les renversements se notent en mettant la basse après une barre de fraction, par exemple pour la triade de ''do'' majeur :
* état fondamental : C ;
* premier renversement : C/E ;
* second renversement : C/G.
|-
! scope="row" | Triade mineure
| Xm, X–
|-
! scope="row" | Septième
| X<sup>7</sup>
|}
{{clear}}
Dans le cas d'un accord de septième de dominante, le nom de l'accord change selon que l'on est en musique classique ou en jazz : en musique classique, on donne le nom de la tonalité alors qu'en jazz, on donne le nom de la fondamentale. Ainsi, l'accord appelé « septième de dominante de ''do'' majeur » en musique classique, est appelé « ''sol'' sept » (G<sup>7</sup>) en jazz : la dominante (degré {{Times New Roman|V}}, dominante) de la tonalité de ''do'' majeur est la note ''sol''.
Comment appelle-t-on en musique classique l'accord appelé « ''do'' sept » (C<sup>7</sup>) en jazz ? Les tonalités dont le ''do'' est la dominante sont les tonalités de ''fa'' majeur (''si''♭ à la clef) et de ''fa'' mineur harmonique (''si''♭, ''mi''♭, ''la''♭ et ''ré''♭ à la clef et ''mi''♮ accidentel). Il s'agit donc de l'accord de septième de dominante des tonalités de ''fa'' majeur et ''fa'' mineur harmonique.
{| class="wikitable"
|+ Accords fréquents pour quelques la tonalités majeures
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Accord parfait<br />{{Times New Roman|I<sup>5</sup>}}
! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}}
|-
|''Do'' majeur || || C<br />''do-mi-sol'' || G7<br />''sol-si-ré-fa''
|-
|''Sol'' majeur || ''fa''♯ || G<br />''sol-si-ré'' || D7<br />''ré-fa''♯''-la-do''
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || D<br />''ré-fa''♯''-la'' || A7<br />''la-do''♯''-mi-sol''
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A<br />''la-do''♯''-mi'' || E7<br />''mi-sol''♯''-si-ré''
|-
| ''Fa'' majeur || ''si''♭ || F<br />''fa-la-do'' || C7<br />''do-mi-sol-si''♭
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭<br />''si''♭''-ré-fa'' || F7<br />''fa-la-do-mi''♭
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭<br />''mi''♭''-sol-si''♭ || B♭7<br />''si''♭''-ré-fa-la''♭
|}
{| class="wikitable"
|+ Accords fréquents pour quelques la tonalités mineures harmoniques
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Accord parfait<br />{{Times New Roman|i<sup>5</sup>}}
! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}}
|-
|''La'' mineur<br />harmonique || || Am, A–<br />''la-do-mi'' || E7<br />''mi-sol''♯''-si-ré''
|-
|''Mi'' mineur<br />harmonique || ''fa''♯ || Em, E–<br />''mi-sol-si'' || B7<br />''si-ré''♯''-fa''♯''-la''
|-
|''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || Bm, B–<br />''si-ré-fa''♯ || F♯7<br />''fa''♯''la''♯''-do''♯''-mi''
|-
|''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || F♯m, F♯–<br />''fa''♯''-la-do''♯ || C♯7<br />''do''♯''-mi''♯''-sol''♯''-si''
|-
| ''Ré'' mineur<br />harmonique || ''si''♭ || Dm, D–<br />''ré-fa-la'' || A7<br />''la-do''♯''-mi-sol''
|-
| ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || Gm, G–<br />''sol-si''♭''-ré'' || D7<br />''ré-fa''♯''-la-do''
|-
| ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || Cm, C–<br />''do-mi''♭''-sol'' || G7<br />''sol-si''♮''-ré-fa''
|}
{{clear}}
== Accords sur les degrés d'une gamme ==
=== Harmonisation d'une gamme ===
[[Fichier:Accord trois notes gamme do majeur chiffre.svg|vignette|upright=1.2|Accords de trois note sur la gamme de ''do'' majeur, chiffrés.]]
On peut ainsi construire une triade par degré d'une gamme.
Pour une gamme majeure, les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|V<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|II<sup>5</sup>}}, {{Times New Roman|III<sup>5</sup>}}, {{Times New Roman|VI<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|ii<sup>5</sup>}}, {{Times New Roman|iii<sup>5</sup>}}, {{Times New Roman|vi<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}.
Les accords ont tous une quinte juste à l'exception de l'accord {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} qui a une quinte diminuée, raison pour laquelle le « 5 » est barré. C'est un accord dit « de quinte diminuée ». En jazz, l'accord diminué est noté « dim », « ° », « m<sup>♭5</sup> » ou « <sup>–♭5</sup> ».
Nous avons donc trois types d'accords (dans la notation jazz) : X (triade majeure), Xm (triade mineure) et X° (triade diminuée), la lettre X remplaçant le nom de la note fondamentale.
{{clear}}
[[Fichier:Accord trois notes gamme la mineur chiffre.svg|vignette|upright=1.2|Accords de trois notes sur une gamme de ''la'' mineur harmonique, chiffrés.]]
Pour une gamme mineure harmonique, les accords {{Times New Roman|III<sup>+5</sup>}}, {{Times New Roman|V<sup>♯</sup>}} et {{Times New Roman|VI<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|II<sup><s>5</s></sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|i<sup>5</sup>}}, {{Times New Roman|ii<sup><s>5</s></sup>}}, {{Times New Roman|iv<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}.
Les accords {{Times New Roman|ii<sup><s>5</s></sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} ont une quinte diminuée ; ce sont des accords dits « de quinte diminuée ». L'accord {{Times New Roman|III<sup>+5</sup>}} a une quinte augmentée ; le signe « plus » indique que la note de cinquième, le ''sol'' dièse, est la sensible. En jazz, l'accord est noté « aug » ou « <sup>+</sup> ». Les autres accords ont une quinte juste.
Aux trois accords générés par une gamme majeure (X, Xm et X°), nous voyons ici apparaître un quatrième type d'accord : la triade augmentée X<sup>+</sup>.
Nous remarquons que des gammes ont des accords communs. Par exemple, l'accord {{Times New Roman|ii<sup>5</sup>}} de ''do'' majeur est identique à l'accord {{Times New Roman|iv<sup>5</sup>}} de ''la'' mineur (il s'agit de l'accord Dm).
Quel que soit le mode, les accords construits sur la sensible (accord de quinte diminuée) sont rarement utilisés. S'ils le sont, c'est en tant qu'accord de septième de dominante sans fondamentale (voir ci-après). C'est la raison pour laquelle le chiffrage indique le degré {{Times New Roman|V}} entre guillemets, et non pas le degré {{Times New Roman|VII}} (mais pour des raisons de clarté, nous l'indiquons entre parenthèses au début).
En mode mineur, l'accord de quinte augmentée {{Times New Roman|iii<sup>+5</sup>}} est très peu utilisé (voir plus loin ''[[#Progression_d'accords|Progression d'accords]]''). C'est un accord considéré comme dissonant.
On voit que :
* un accord parfait majeur peut appartenir à cinq gammes différentes ;<br /> par exemple l'accord parfait de ''do'' majeur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> degré de la gamme de ''do'' majeur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' mineur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''mi'' mineur ;
* un accord parfait mineur peut appartenir à cinq gammes différentes ;<br />par exemple l'accord parfait de ''la'' mineur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> de la gamme de ''la'' mineur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''mi'' mineur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|III}}<sup>e</sup> degré de ''fa'' majeur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''do'' majeur ;
* un accord de quinte diminuée peut appartenir à trois gammes différentes ;<br />par exemple, l'accord de quinte diminuée de ''si'' est l'accord construit sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' majeur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''la'' mineur et sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' mineur ;
* un accord de quinte augmentée (à l'état fondamental) ne peut appartenir qu'à une seule gamme ;<br /> par exemple, l'accord de quinte augmentée de ''do'' est l'accord construit sur le {{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur.
{| class="wikitable"
|+ Notation jazz des triades
|-
| rowspan="2" colspan="2" |
! scope="col" colspan="2" | Tierce
|-
! scope="col" | 3m
! scope="col" | 3M
|-
! rowspan="3" | Quinte
! scope="row" | 5d
| Xᵒ, X<sub>m</sub><sup>(♭5)</sup> ||
|-
! scope="row" | 5J
| Xm, X– || X
|-
! scope="row" | 5A
| || X+, X<sup>(♯5)</sup>
|}
=== Harmonisation par des accords de septième ===
[[Fichier:Harmonisation gamme do majeur par septiemes chiffre.svg|vignette|upright=2|Harmonisation de la gamme de do majeur par des accords de septième.]]
Les accords de septième contiennent une dissonance et créent ainsi une tension. Ils sont très utilisés en jazz. Nous avons représenté ci-contre l'harmonisation de la gamme de ''do'' majeur.
La constitution des accords est la suivantes :
* tierce majeure (3M)
** quinte juste (5J)
*** septième mineure (7m) : sur le degré V, c'est l'accord de septième de dominante V<sup>7</sup><sub>+</sub>, noté X<sup>7</sup> (X pour G),
*** septième majeure (7M) : sur les degrés I et IV, appelés « accords de septième majeure » et notés aussi X<sup>maj7</sup> ou X<sup>Δ</sup> (X pour C ou F) ;
* tierce mineure (3m)
** quinte juste (5J)
*** septième mineure : sur les degrés ii, iii et vi, appelés « accords mineur septième » et notés Xm<sup>7</sup> ou X–<sup>7</sup> (X pour D, E ou A),
** quinte diminuée (5d)
*** septième mineure (7m) : sur le degré vii, appelé « accord demi-diminué » (puisque seule la quinte est diminuée) et noté X<sup>∅</sup> ou Xm<sup>7(♭5)</sup> ou X–<sup>7(♭5)</sup> (X pour B) ;<br /> en musique classique, on considère que c'est un accord de neuvième de dominante sans fondamentale.
Nous avons donc quatre types d'accords : X<sup>7</sup>, X<sup>maj7</sup>, Xm<sup>7</sup> et X<sup>∅</sup>
En jazz, on ajoute souvent la quarte à l'accord de sous-dominante IV (sur le ''fa'' dans une gamme de ''do'' majeur) ; il s'agit ici d'une quarte augmentée (''fa''-''si'') et l'accord est surnommé « accord lydien » mais cette dénomination est erronée (il s'agit d'une mauvaise interprétation de textes antiques). C'est un accord de onzième sans neuvième (la onzième étant l'octave de la quarte), il est noté X<sup>maj7(♯11)</sup> ou X<sup>Δ(♯11)</sup> (ici, F<sup>maj7(♯11)</sup>, ''fa''-''la''-''do''-''mi''-''si'' ou ''fa''-''la''-''si''-''do''-''mi'').
{| class="wikitable"
|+ Chiffrage jazz des accords de septième
|-
! scope="col" rowspan="2" | Tierce
! scope="col" rowspan="2" | Quinte
! scope="col" | Septième
|-
! scope="col" | 7m
! scope="col" | 7M
|-
| rowspan="2" | 3m
| 5d || 7m || X<sup>∅</sup>, Xm<sup>7(♭5)</sup> ||
|-
| X<sup>7</sup>
|-
| rowspan="2" | 3M
| 5J || X<sup>7</sup> || X<sup>maj7</sup>, X<sup>Δ</sup>
|-
| 5A || || X<sub>+</sub><sup>maj7</sup>, X<sub>+</sub><sup>Δ</sup>
|}
Note : le « m » peut être remplacé par un signe moins « – ».
=== Modulation et emprunt ===
Un morceau peut comporter des changements de tonalité; appelés « modulation ». Il y a parfois un court passage dans une tonalité différente, typiquement sur une ou deux mesures, avant de retourner dans la tonalité d'origine : on parle d'emprunt. Lorsqu'il y a une modulation ou un emprunt, les degrés changent. Un même accord peut donc avoir une fonction dans une partie du morceau et une autre fonction ailleurs. L'utilisation d'accord différents, et en particulier d'accord utilisant des altérations accidentelles, indique clairement une modulation.
Nous avons vu précédemment que les modulations courantes sont :
* les modulations dans les tons voisins ;
* les modulations homonymes ;
* les marches harmoniques.
Une modulation entre une tonalité majeure et mineure change la couleur du passage,
* la modulation la plus « douce » est entre les tonalités relatives (par exemple''do'' majeur et ''la'' mineur) car ces tonalités utilisent quasiment les mêmes notes ;
* la modulation la plus « voyante » est la modulation homonyme (par exemple entre ''do'' majeur et ''do'' mineur).
Une modulation commence souvent sur l'accord de dominante de la nouvelle tonalité.
Pour analyser un œuvre, ou pour improviser sur une partie, il est important de reconnaître les modulations. La description de la successind es tonalités s'appelle le « parcours tonal ».
=== Exercices élémentaires ===
L'apprentissage des accords passe par quelques exercices élémentaires.
'''1. Lire un accord'''
Il s'agit de lecture de notes : des notes composant les accords sont écrites « empilées » sur une portée, il faut les lire en énonçant les notes de bas en haut.
'''2. Reconnaître la « couleur » d'un accord'''
On écoute une triade et il faut dire si c'est une triade majeure ou mineure. Puis, on complexifie l'exercice en ajoutant la septième.
'''3. Chiffrage un accord'''
Trouver le nom d'un accord à partir des notes qui le composent.
'''4. Réalisation d'un accord'''
Trouver les notes qui composent un accord à partir de son nom.
'''5. Dictée d'accords'''
On écoute une succession d'accords et il faut soit écrire les notes sur une portée, soit écrire les noms de accords.
[[File:Exercice constitution accord basse chiffree.svg|thumb|Exercice : constitution d'accord à partir de la basse chiffrée.]]
'''Exercices de basse chiffrée'''
''Réalisation d'un accord''
Sur la figure suivante, écrire les notes des accords correspondant à la basse chiffrée. Déterminer le degré de la fondamentale pour chaque accord en considérant que nous sommes dans la tonalité de ''sol'' majeur.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice constitution accord basse chiffree solution.svg|vignette|Solution.]]
# La note de basse est un ''do''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''mi'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''sol''.<br />Le chiffrage « <sup>5</sup> » indique que c'est un accord dans son état fondamental (l'écart entre deux notes consécutives ne dépasse pas la tierce), la fondamentale est donc la basse, ''do'', qui est le degré IV de la tonalité.
# La note de basse est un ''si''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''ré'', puis nous appliquons le chiffrage 6 et ajoutons la sixte, ''sol''.<br />Le chiffrage « <sup>6</sup> » indique que c'est un accord dans son premier renversement. En le remettant dans son état fondamental, nous obtenons ''sol-si-ré'', la fondamentale est donc la tonique, le degré I.
# La note de basse est un ''la''. Nous ajoutons la tierce (chiffre 3), ''do'', et la sixte (6), ''fa''♯. Nous vérifions que le ''fa''♯ est la sensible (signe +)<br />Nous voyons un « blanc » entre les notes ''do'' et ''fa''♯. En descendant le ''fa''♯ à l'octave inférieure, nous obtenons un empilement de tierces ''fa''♯-''la-do'', le fondamentale est donc ''fa''♯, le degré VII. Nous pouvons le voir comme le deuxième renversement de l'accord de septième de dominante, sans fondamentale.
# La note de basse est un ''fa''♯. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''la'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''do'' ; nous vérifions qu'il s'agit bien d'une quinte diminuée (le 5 est barré). Nous appliquons le chiffre 6 et ajoutons la sixte, ''ré''.<br />Nous voyons que les notes ''do'' et ''ré'' sont conjointes (intervalle de seconde). En descendant le ''ré'' à l'octave inférieure, nous obtenons un empilement de tierces ''ré-fa''♯-''la-do'', le fondamentale est donc ''ré'', le degré V. Nous constatons que l'accord chiffré est le premier renversement de l'accord de septième de dominante.
{{boîte déroulante/fin}}
{{clear}}
''Chiffrage d'accords''
[[Fichier:Exercice chiffrage accord basse chiffree.svg|vignette|Accords à chiffrer.]]
Chiffrer les accords ci-contre.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice chiffrage accord basse chiffree solution.svg|vignette|Solution.]]
# On relève les intervalles en partant de la basse : tierce majeure (3M) et quinte juste (5J). Le chiffrage complet est donc ''fa''<sup>5</sup><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''fa''<sup>5</sup>.<br /> On peut aussi reconnaître que c'est l'accord parfait sur la tonique de la tonalité de ''fa'' majeur dans son état fondamental, le chiffrage d'un accord parfait étant <sup>5</sup>.
# On relève les intervalles en partant de la basse : quarte juste (4J), sixte majeure (6M). Le chiffrage complet est donc ''fa''<sup>6</sup><sub>4</sub>.<br /> On peut aussi reconnaître que c'est le second renversement de l'accord ''mi-sol-si'', sur la tonique de la tonalité de ''mi'' mineur, le chiffrage du second renversement d'un accord parfait étant <sup>6</sup><sub>4</sub>.
# Les intervalles en partant de la basse sont : tierce mineure (3m), quinte diminuée (5d), sixte mineure (6m). Le chiffrage complet est donc ''mi''<sup>6</sup><small><s>5</s></small><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''mi''<sup>6</sup><sub><s>5</s></sub>.<br /> On reconnaît le premier renversement de l'accord ''do-mi-sol-si''♭, accord de septième de dominante de la tonalité de ''fa'' majeur.
# Les intervalles en partant de la basse sont : tierce mineure (3m), quinte juste (5J), septième mineure (7m). Le chiffrage complet est donc ''ré''<sup>7</sup><small>5</small><sub>3</sub> ; c'est typique d'un accord de septième de dominante, son chiffrage est donc ''ré''<sup>7</sup><sub>+</sub>.<br /> On reconnaît l'accord de septième de dominante de la tonalité de ''sol'' mineur dans son état fondamental.
{{boîte déroulante/fin}}
{{clear}}
[[File:Exercice constitution accord notation jazz.svg|thumb|Exercice : constitution d'un accord d'après son chiffrage en notation jazz.]]
'''Exercices de notation jazz'''
''Réalisation d'un accord''
Sur la figure suivante, écrire les notes des accords correspondant aux chiffrages.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice constitution accord notation jazz solution.svg|thumb|Solution.]]
# Il s'agit de la triade majeure de ''do'' dans son état fondamental. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''do-mi-sol''.
# Il s'agit de la triade majeure de ''sol''. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''sol-si-ré''. On renverse l'accord afin que la basse soit le ''si'', l'accord est donc ''si-ré-sol''.
# Il s'agit de l'accord demi-diminué de ''fa''♯. Les intervalles sont la tierce mineure (3m), la quinte diminuée (5d) et la septième mineure (7m). Les notes sont donc ''fa''♯-''la-do-mi''. Nous renversons l'accord afin que la basse soit le ''la'', l'accord est donc ''a-do-mi-fa''♯.
# Il s'agit de l'accord de septième de ''ré''. Les intervalles sont donc la tierce majeure (3M), la quinte juste (5J) et la septième mineure (7m). Les notes sont ''ré-fa''♯''-la-do''. Nous renversons l'accord afin que la basse soit le ''fa''♯, l'accord est donc ''fa''♯''-la-do-ré''.
{{boîte déroulante/fin}}
{{clear}}
''Chiffrage d'accords''
[[File:Exercice chiffrage accord notation jazz.svg|thumb|Accords à chiffrer.]]
Chiffrer les accords ci-contre.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice chiffrage accord notation jazz solution.svg|thumb|Solution.]]
# Les notes sont toutes sur des interlignes consécutifs, c'est donc un empilement de tierces ; l'accord est dans son état fondamental. Les intervalles sont une tierce majeure (''fa-la'' : 3M) et une quinte juste (''fa-do'' : 5J), c'est donc la triade majeure de ''fa''. Le chiffrage est F.
# Il y a un blanc dans l'empilement des notes, c'est donc un accord renversé. En permutant les notes pour n'avoir que des tierces, on trouve l'accord ''mi-sol-si''. Les intervalles sont une tierce mineure (''mi-sol'' : 3m) et une quinte juste (''mi-si'' : 5J), c'est donc la triade mineure de ''mi'' avec un ''si'' à la basse. Le chiffrage est Em/B ou E–/B.
# Il y deux notes conjointes, c'est donc un renversement. L'état fondamental de cet accord est ''do-mi-sol-si''♭. Les intervalles sont une tierce majeure (''do-mi'' : 3M), une quinte juste (''do-sol'' : 5J) et une septième mineure (''do-si''♭ : 7m). C'est donc l'accord de ''do'' septième avec un ''mi'' à la basse, chiffré C<sup>7</sup>/E.
# Les notes sont toutes sur des interlignes consécutifs, l'accord est dans son état fondamental. Les intervalles sont la tierce mineure (''ré-fa'' : 3m), une quinte juste (''ré-la'' : 5J) et une septième mineure (''ré-do'' : 7m). C'est donc l'accord de ''ré'' mineur septième, chiffré Dm<sup>7</sup> ou D–<sup>7</sup>.
{{boîte déroulante/fin}}
{{clear}}
== Harmonie fonctionnelle ==
Le choix des accords et de leur succession — la progression des accords — est un élément important d'un morceau, de sa composition. Le compositeur ou la compositrice a bien sûr une liberté totale, mais pour faire des choix, il faut comprendre les conséquences de ces choix, et donc ici, les effets produits par les accords et leur progression.
Une des manières d'aborder le sujet est l'harmonie fonctionnelle.
=== Les trois fonctions des accords ===
En harmonie tonale, on considère que les accords ont une fonction. Il existe trois fonctions :
* la fonction de tonique, {{Times New Roman|I}} ;
* la fonction de sous-dominante, {{Times New Roman|IV}} ;
* la fonction de dominante, {{Times New Roman|V}}.
L'accord de tonique, {{Times New Roman|I}}, est l'accord « stable » de la tonalité par excellence. Il conclut en général les morceaux, et ouvre souvent les morceaux ; il revient fréquemment au cours du morceau.
L'accord de dominante, {{Times New Roman|V}}, est un accord qui introduit une instabilité, une tension. En particulier, il contient la sensible (degré {{Times New Roman|VI}}), qui est une note « aspirée » vers la tonique. Cette tension, qui peut être renforcée par l'utilisation d'un accord de septième, est fréquemment résolue par un passage vers l'accord de tonique. Nous avons donc deux mouvements typiques : {{Times New Roman|I}} → {{Times New Roman|V}} (création d'une tension, d'une attente) et {{Times New Roman|V}} → {{Times New Roman|I}} (résolution d'une tension). Les accords de tonique et de dominante ont le cinquième degré en commun, cette note sert donc de pivot entre les deux accords.
L'accord de sous-dominante, {{Times New Roman|IV}}, est un accord qui introduit lui aussi une tension, mais moins grande : il ne contient pas la sensible. Notons que s'il est une quarte au-dessus de la tonique, il est aussi une quinte en dessous d'elle ; il est symétrique de l'accord de dominante. Il a donc un rôle similaire à l'accord de dominante, mais atténué. L'accord de sous-dominante aspire soit vers l'accord de dominante, très proche, et l'on a alors une augmentation de la tension ; soit vers l'accord de tonique, un retour vers la stabilité (il a alors un rôle semblable à la dominante). Du fait de ces deux bifurcations possibles — augmentation de la tension ({{Times New Roman|IV}} → {{Times New Roman|V}}) ou retour à la stabilité ({{Times New Roman|IV}} → {{Times New Roman|I}}) —, l'utilisation de l'accord de sous-dominante introduit un certain flottement : si l'on peut facilement prédire l'accord qui suit un accord de dominante, on ne peut pas prédire ce qui suit un accord de sous-dominante.
Notons que la composition ne consiste pas à suivre ces règles de manière stricte, ce qui conduirait à des morceaux stéréotypés et plats. Le plaisir d'écoute joue sur une alternance entre satisfaction d'une attente (respect des règles) et surprise (rompre les règles).
=== Accords remplissant ces fonctions ===
Les accords sur les autres degrés peuvent se ramener à une de ces trois fonctions :
* {{Times New Roman|II}} : fonction de sous-dominante {{Times New Roman|IV}} ;
* {{Times New Roman|III}} (très peu utilisé en mode mineur en raison de sa dissonance) et {{Times New Roman|VI}} : fonction de tonique {{Times New Roman|I}} ;
* {{Times New Roman|VII}} : fonction de dominante {{Times New Roman|V}}.
En effet, les accords étant des empilements de tierces, des accords situés à une tierce l'un de l'autre — {{Times New Roman|I}} ↔ {{Times New Roman|III}}, {{Times New Roman|II}} ↔ {{Times New Roman|IV}}, {{Times New Roman|V}} ↔ {{Times New Roman|VII}}, {{Times New Roman|VI}} ↔ {{Times New Roman|VIII}} ( = {{Times New Roman|I}}) — ont deux notes en commun. On retrouve le fait que l'accord sur le degré {{Times New Roman|VII}} est considéré comme un accord de dominante sans tonique. En mode mineur, l'accord sur le degré {{Times New Roman|III}} est évité, il n'a donc pas de fonction.
{|class="wikitable"
|+ Fonction des accords
|-
! scope="col" | Fondamentale
! scope="col" | Fonction
|-
| {{Times New Roman|I}} || tonique
|-
| {{Times New Roman|II}} || sous-dominante faible
|-
| {{Times New Roman|III}} || tonique faible
|-
| {{Times New Roman|IV}} || sous-dominante
|-
| {{Times New Roman|V}} || dominante
|-
| {{Times New Roman|VI}} || tonique faible
|-
| {{Times New Roman|VII}} || dominante faible
|}
Par exemple en ''do'' majeur :
* fonction de tonique : '''''do''<sup>5</sup> (C)''', ''mi''<sup>5</sup> (E–), ''la''<sup>5</sup> (A–) ;
* fonction de sous-dominante : '''''fa''<sup>5</sup> (F)''', ''ré''<sup>5</sup> (D–) ;
* fonction de dominante : '''''sol''<sup>5</sup> (G)''' ou ''sol''<sup>7</sup><sub>+</sub> (G<sup>7</sup>), ''si''<sup> <s>5</s></sup> (B<sup>o</sup>).
En ''la'' mineur harmonique :
* fonction de tonique : '''''la''<sup>5</sup> (A–)''', ''fa''<sup>5</sup> (F) [, rarement : ''do''<sup>+5</sup> (C<sup>+</sup>)] ;
* fonction de sous-dominante : '''''ré''<sup>5</sup> (D–)''', ''si''<sup> <s>5</s></sup> (B<sup>o</sup>) ;
* fonction de dominante : '''''mi''<sup>5</sup> (E)''' ou ''mi''<sup>7</sup><sub>+</sub> (E<sup>7</sup>), ''sol''♯<sup> <s>5</s></sup> (G♯<sup>o</sup>).
Le fait d'utiliser des accords différents pour remplir une fonction permet d'enrichir l'harmonie, et de jouer sur l'équilibre entre satisfaction d'une attente (on respecte les règles sur les fonctions) et surprise (mais on n'utilise pas l'accord attendu).
=== Les dominantes secondaires ===
On utilise aussi des accords de septième dominante se fondant sur un autre degré que la dominante de la gamme ; on parle de « dominante secondaire ». Typiquement, avant un accord de septième de dominante, on utilise parfois un accord de dominante de dominante, dont le degré est alors noté « {{Times New Roman|V}} de {{Times New Roman|V}} » ou « {{Times New Roman|V}}/{{Times New Roman|V}} » ; la fondamentale est de l'accord est alors situé cinq degrés au-dessus de la dominante ({{Times New Roman|V}}), c'est donc le degré {{Times New Roman|IX}}, c'est-à-dire le degré {{Times New Roman|II}} de la tonalité en cours). Ou encore, on utilise un accord de dominante du degré {{Times New Roman|IV}} (« {{Times New Roman|V}} de {{Times New Roman|IV}} », la fondamentale est alors le degré {{Times New Roman|I}}) avant un accord sur le degré {{Times New Roman|IV}} lui-même.
Par exemple, en tonalité de ''do'' majeur, on peut trouver un accord ''ré - fa''♯'' - la - do'' (chiffré {{Times New Roman|V}} de {{Times New Roman|V}}<sup>7</sup><sub>+</sub>), avant un accord ''sol - si - ré - fa'' ({{Times New Roman|V}}<sup>7</sup><sub>+</sub>). L'accord ''ré - fa''♯'' - la - do'' est l'accord de septième de dominante des tonalités de ''sol''. Dans la même tonalité, on pourra utiliser un accord ''do - mi - sol - si''♭ ({{Times New Roman|V}} de {{Times New Roman|IV}}<sup>7</sup><sub>+</sub>) avant un accord ''fa - la - do'' ({{Times New Roman|IV}}<sup>5</sup>). Le recours à une dominante secondaire peut atténuer une transition, par exemple avec un enchaînement ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> → ''fa''<sup>5</sup> (C → C<sup>7</sup> → F) qui correspond à un enchaînement {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}} : le passage ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> (C → C<sup>7</sup>) se fait en ajoutant une note (le ''si''♭) et rend naturel le passage ''do'' → ''fa''.
Sur les sept degré de la gamme, on ne considère en général que cinq dominantes secondaires : en effet, la dominante du degré {{Times New Roman|I}} est la dominante « naturelle, primaire » de la tonalité (et n'est donc pas secondaire) ; et utiliser la dominante de {{Times New Roman|VII}} consisterait à considérer l'accord de {{Times New Roman|VII}} comme un accord propre, on évite donc les « {{Times New Roman|V}} de “{{Times New Roman|V}}” » (mais les « “{{Times New Roman|V}}” de {{Times New Roman|V}} » sont tout à fait « acceptables »).
=== Enchaînements classiques ===
Nous avons donc vu que l'on trouve fréquemment les enchaînements suivants :
* pour créer une instabilité :
** {{Times New Roman|I}} → {{Times New Roman|V}},
** {{Times New Roman|I}} → {{Times New Roman|IV}} (instabilité moins forte mais incertitude sur le sens d'évolution) ;
* pour maintenir l'instabilité :
** {{Times New Roman|IV}} → {{Times New Roman|V}} ;
* pour résoudre l'instabilité :
** {{Times New Roman|IV}} → {{Times New Roman|I}},
** {{Times New Roman|V}} → {{Times New Roman|I}}, cas particuliers (voir plus bas) :
*** {{Times New Roman|V}}<sup>+4</sup> → {{Times New Roman|I}}<sup>6</sup>,
*** {{Times New Roman|I}}<sup>6</sup><sub>4</sub> → {{Times New Roman|V}}<sup>7</sup><sub>+</sub> → {{Times New Roman|I}}<sup>5</sup>.
Les degrés indiqués ci-dessus sont les fonctions ; on peut donc utiliser les substitutions suivantes :
* {{Times New Roman|I}} par {{Times New Roman|VI}} et, en tonalité majeure, {{Times New Roman|III}} ;
* {{Times New Roman|IV}} par {{Times New Roman|II}} ;
* {{Times New Roman|V}} par {{Times New Roman|VII}}.
Pour enrichir l'harmonie, on peut utiliser les dominantes secondaires, en particulier :
* {{Times New Roman|V}} de {{Times New Roman|V}} ({{Times New Roman|II}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|V}},
* {{Times New Roman|V}} de {{Times New Roman|IV}} ({{Times New Roman|I}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|IV}}.
On peut enchaîner les enchaînements, par exemple {{Times New Roman|I}} → {{Times New Roman|IV}} → {{Times New Roman|V}}, ou encore {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}}… En jazz, on utilise très fréquemment l'enchaînement {{Times New Roman|II}} → {{Times New Roman|V}} → {{Times New Roman|I}} (deux-cinq-un).
On peut bien sûr avoir d'autres enchaînements, mais ces règles permettent d'analyser un grand nombre de morceaux, et donnent des clefs utiles pour la composition. Nous voyons ci-après un certain nombre d'enchaînements courants dans différents styles
== Exercice ==
Un hautboïste travaille la sonate en ''do'' mineur S. 277 de Heinichen. Sur le deuxième mouvement ''Allegro'', il a du mal à travailler un passage en raison des altérations accidentelles. Sur la suggestion de sa professeure, il décide d'analyser la progression d'accords sous-jacente afin que les altérations deviennent logiques. Il s'agit d'un duo hautbois et basson pour lequel les accords ne sont pas chiffrés, le basson étant ici un instrument soliste et non pas un élément de la basse continue.
Sur l'extrait suivant, déterminez les basses et la qualité (chiffrage) des accords sous-jacents. Commentez.
[[Fichier:Sonate hautbois basson heinichen 2e mvt mes49.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen.]]
{{note|L'œuvre est en ''do'' mineur et devrait donc avoir trois bémols à la clef, or ici il n'y en a que deux. En effet, le ''la'' pouvant être bécarre en mode mineur mélodique ascendant, le compositeur a préféré le noter explicitement en altération accidentelle lorsque l'on est en mode mélodique naturel, harmonique ou mélodique descendant. C'est un procédé assez courant à l'époque baroque.}}
{{boîte déroulante/début|titre=Solution}}
Une des difficultés ici est que les arpèges joués par les instruments sont agrémentés de notes de passage.
Les notes de la basse (du basson) sont différentes entre le premier et le deuxième temps de chaque mesure et ne peuvent pas appartenir au même accord. On a donc un accord par temps.
Sur le premier temps de chaque mesure, le basson joue une octave. La note concernée est donc la basse de chaque accord. Pour savoir s'il s'agit d'un accord à l'état fondamental ou d'un renversement, on regarde ce que joue le hautbois : dans un mouvement conjoint (succession d'intervalles de secondes), il est difficile de distinguer les notes de l'arpège des notes de passage, mais
: les notes des grands intervalles font partie de l'accord.
Ainsi, sur le premier temps de la première mesure (la basse est un ''mi''♭), on a une sixte descendante ''sol''-''si''♭ et, à la fin du temps, une tierce descendante ''sol''-''mi''♭. L'accord est donc ''mi''♭-''sol''-''si''♭, c'est un accord de quinte (accord parfait à l'état fondamental). À la fin du premier temps, le basson joue un ''do'', c'est donc une note étrangère.
Sur le second temps de la première mesure, le basson joue une tierce ascendante ''fa''-''la''♭, la première note est la basse de l'accord et la seconde une des notes de l'accord. Le hautbois commence par une sixte descendante ''la''♭-''do'', l'accord est donc ''fa''-''la''♭-''do'', un accord de quinte (accord parfait à l'état fondamental). Le ''do'' du basson la fin du premier temps est donc une anticipation.
Les autres notes étrangères de la première mesure sont des notes de passage.
Mais il faut faire attention : en suivant ce principe, sur les premiers temps des deuxième et troisième mesure, nous aurions des accords de septième d'espèce (puisque la septième est majeure). Or, on ne trouve pas, ou alors exceptionnellement, d'accord de septième d'espèce dans le baroque, mais quasi exclusivement des accords de septième de dominante. Donc au début de la deuxième mesure, le ''la''♮ est une appoggiature du ''si''♭, l'accord est donc ''si''♭-''ré''-''fa'', un asscord de quinte. De même, au début de la troisième mesure, le ''sol'' est une appoggiature du ''la''♭.
Il faut donc se méfier d'une analyse purement « mathématique ». Il faut s'attacher à ressentir la musique, et à connaître les styles, pour faire une analyse pertinente.
Ci-dessous, nous avons grisé les notes étrangères.
[[Fichier:Sonate hautbois basson heinichen 2e mvt mes49 analyse.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen. Analyse de la progression harmonique.]]
Le chiffrage jazz équivalent est :
: | E♭ F– | B♭<sup>Δ</sup> E♭ | A♭<sup>Δ</sup> D– | G …
Nous remarquons une progression assez régulière :
: ''mi''♭ ↗[2<sup>de</sup>] ''fa'' | ↘[5<sup>te</sup>] ''si''♭ ↗[4<sup>te</sup>] ''mi''♭ | ↘[5<sup>te</sup>] ''la''♭ ↗[4<sup>te</sup>] ''ré'' | ↘[5<sup>te</sup>] ''sol''
Le ''mi''♭ est le degré {{Times New Roman|III}} de la tonalité principale (''do'' mineur), c'est donc une tonique faible ; il « joue le même rôle » qu'un ''do''. S'il y avait eu un accord de ''do'' au début de l'extrait, on aurait eu une progression parfaitement régulière ↗[4<sup>te</sup>] ↘[5<sup>te</sup>].
Nous avons les modulations suivantes :
* mesure 49 : ''do'' mineur naturel (le ''si''♭ n'est pas une sensible) avec un accord sur “{{Times New Roman|I}}” (tonique faible, {{Times New Roman|III}}, pour la première analyse, ou bien tonique forte, {{Times New Roman|I}}, pour la seconde) suivi d'un accord sur {{Times New Roman|IV}} ;
* mesure 50 : ''si''♭ majeur avec un accord sur {{Times New Roman|I}} suivi d'un accord sur {{Times New Roman|IV}} ;
* mesure 51 : ''la''♭ majeur avec un accord sur {{Times New Roman|I}}, et emprunt à ''do'' majeur avec un accord sur {{Times New Roman|II}} ({{Times New Roman|IV}} faible).
On a donc une marche harmonique {{Times New Roman|I}} → {{Times New Roman|IV}} qui descend d'une seconde majeure (un ton) à chaque mesure (''do'' → ''si''♭ → ''la''♭), avec une exception sur la dernière mesure (modulation en cours de mesure et descente d'une seconde mineure au lieu de majeure).
Ce passage est donc construit sur une régularité, une règle qui crée un effet d'attente — enchaînement {{Times New Roman|I}}<sup>5</sup> → {{Times New Roman|IV}}<sup>5</sup> avec une marche harmonique d'une seconde majeure descendante —, et des « surprises », des exceptions au début — ce n'est pas un accord {{Times New Roman|I}}<sup>5</sup> mais un accord {{Times New Roman|III}}<sup>5</sup> — et à la fin — modulation en milieu de mesure et dernière descente d'une seconde mineure (½t ''la''♭ → ''sol'').
L'extrait ne permet pas de le deviner, mais la mesure 52 est un retour en ''do'' mineur, avec donc une modulation sur la dominante (accord de ''sol''<sup>7</sup><sub>+</sub>, G<sup>7</sup>).
{{boîte déroulante/fin}}
== Progression d'accords ==
Comme pour la mélodie, la succession des accords dans un morceau, la progression d'accords, suit des règles. Et comme pour la mélodie, les règles diffèrent d'un style musical à l'autre et la créativité consiste à parfois ne pas suivre ces règles. Et comme pour la mélodie, on part d'un ensemble de notes organisé, d'une gamme caractéristique d'une tonalité, d'un mode.
Les accords les plus utilisés pour une tonalité donnée sont les accords dont la fondamentale sont les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la tonalité, en particulier la triade {{Times New Roman|I}}, appelée « accord parfait » ou « accord de tonique », et l'accord de septième {{Times New Roman|V}}, appelé « septième de dominante ».
Le fait d'avoir une progression d'accords qui se répète permet de structurer un morceau. Pour les morceaux courts, il participe au plaisir de l'écoute et facilite la mémorisation (par exemple le découpage couplet-refrain d'une chanson). Sur les morceaux longs, une trop grande régularité peut introduire de la lassitude, les longs morceaux sont souvent découpés en parties présentant chacune une progression régulière. Le fait d'avoir une progression régulière permet la pratique de l'improvisation : cadence en musique classique, solo en jazz et blues.
; Note
: Le terme « cadence » désigne plusieurs choses différentes, et notamment en harmonie :
:* une partie improvisée dans un opéra ou un concerto, sens utilisé ci-dessus ;
:* une progression d'accords pour ponctuer un morceau et en particulier pour le conclure, sens utilisé dans la section suivante.
=== Accords peu utilisés ===
En mode mineur, l'accord de quinte augmentée {{Times New Roman|III<sup>+5</sup>}} est très peu utilisé. C'est un accord dissonant ; il intervient en général comme appogiature de l'accord de tonique (par exemple en ''la'' mineur : {{Times New Roman|III<sup>+5</sup>}} ''do'' - ''mi'' - ''sol''♯ → {{Times New Roman|I<sup>6</sup>}} ''do'' - ''mi'' - ''la''), ou de l'accord de dominante ({{Times New Roman|III<sup>6</sup><sub>+3</sub>}} ''mi'' - ''sol''♯ - ''do'' → {{Times New Roman|V<sup>5</sup>}} ''mi'' - ''sol''♯ - ''si''). Il peut être aussi utilisé comme préparation à l'accord de sous-dominante (enchaînement {{Times New Roman|III}} → {{Times New Roman|IV}}). Par ailleurs, il a une constitution symétrique — c'est l'empilement de deux tierces majeures — et ses renversements ont les mêmes intervalles à l'enharmonie près (quinte augmentée/sixte mineure, tierce majeure/quarte diminuée). De ce fait, un même accord est commun, par renversement et à l'enharmonie près, à trois tonalités : le premier renversement de l'accord ''do'' - ''mi'' - ''sol''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur) est enharmonique à ''mi'' - ''sol''♯ - ''si''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''do''♯ mineur) ; le second renversement est enharmonique à ''la''♭ - ''do'' - ''mi'' ({{Times New Roman|III}}<sup>e</sup> degré de ''fa'' mineur).
=== Accords très utilisés ===
Les trois accords les plus utilisés sont les accords de tonique (degré {{Times New Roman|I}}), de sous-dominante ({{Times New Roman|IV}}) et de dominante ({{Times New Roman|V}}). Ils interviennent en particulier en fin de phrase, dans les cadences. L'accord de dominante sert souvent à introduire une modulation : la modulation commence sur l'accord de dominante de la nouvelle tonalité. On note que l'accord de sous-dominante est situé une quinte juste en dessous de la tonique, les accords de dominante et de sous-dominante sont donc symétriques.
En jazz, on utilise également très fréquemment l'accord de la sus-tonique (degré {{Times New Roman|II}}), souvent dans des progressions {{Times New Roman|II}} - {{Times New Roman|V}} (- {{Times New Roman|I}}). Rappelons que l'accord de sus-tonique a la fonction de sous-dominante.
=== Cadences et ''turnaround'' ===
Le terme « cadence » provient de l'italien ''cadenza'' et désigne la « chute », la fin d'un morceau ou d'une phrase musicale.
On distingue deux types de cadences :
* les cadences conclusive, qui créent une sensation de complétude ;
* les cadences suspensives, qui crèent une sensation d'attente.
==== Cadence parfaite ====
[[Fichier:Au clair de le lune cadence parfaite.midi|thumb|''Au clair de la lune'', harmonisé avec une cadence parfaite (italienne).]]
[[Fichier:Au clair de le lune mineur cadence parfaite.midi|thumb|''Idem'' mais en mode mineur harmonique.]]
La cadence parfaite est l'enchaînement de l'accord de dominante suivi de l'accord parfait : {{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}}, les deux accord étant à l'état fondamental. Elle donne une impression de stabilité et est donc très souvent utilisée pour conclure un morceau. C'est une cadence conclusive.
On peut aussi utiliser l'accord de septième de dominante, la dissonance introduisant une tension résolue par l'accord parfait : {{Times New Roman|V<sup>7</sup><sub>+</sub> - I<sup>5</sup>}}.
Elle est souvent précédée de l'accord construit sur le IV<sup>e</sup> degré, appelé « accord de préparation », pour former la cadence italienne : {{Times New Roman|IV<sup>5</sup> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}}.
Elle est également souvent précédée du second renversement de l'accord de tonique, qui est alors appelé « appoggiature de la cadence » : {{Times New Roman|I<sup>6</sup><sub>4</sub> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}} (on remarque que les accords {{Times New Roman|I}}<sup>6</sup><sub>4</sub> et {{Times New Roman|V}}<sup>5</sup> ont la basse en commun, et que l'on peut passer de l'un à l'autre par un mouvement conjoint sur les autres notes).
{{clear}}
==== Demi-cadence ====
[[Fichier:Au clair de le lune demi cadence.midi|thumb|''Au clair de la lune'', harmonisé avec une demi-cadence.]]
Une demi-cadence est une phrase ou un morceau se concluant sur l'accord construit sur le cinquième degré. Il provoque une sensation d'attente, de suspens. Il s'agit en général d'une succession {{Times New Roman|II - V}} ou {{Times New Roman|IV - V}}. C'est une cadence suspensive. On uilise rarement un accord de septième de dominante.
{{clear}}
==== Cadence rompue ou évitée ====
La cadence rompue, ou cadence évitée, est succession d'un accord de dominante et d'un accord de sus-dominante, {{Times New Roman|V}} - {{Times New Roman|VI}}. C'est une cadence suspensive.
==== Cadence imparfaite ====
Une cadence imparfaite est une cadence {{Times New Roman|V - I}}, comme la cadence parfaite, mais dont au moins un des deux accords est dans un état renversé.
==== Cadence plagale ====
La cadence plagale — du grec ''plagios'', oblique, en biais — est la succession de l'accord construit sur le quatrième degré, suivi de l'accord parfait : {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}. Elle peut être utilisée après une cadence parfaite ({{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}} - {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}). Elle donne un caractère solennel, voire religieux — elle est parfois appelée « cadence amen » —, elle a un côté antique qui rappelle la musique modale et médiévale<ref>{{lien web |url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/la-cadence-amen-ou-comment-se-dire-adieu-7191921 |titre=La cadence « Amen » ou comment se dire adieu |auteur=Max Dozolme (MAXXI Classique) |site=France Musique |date=2025-04-25 |consulté le=2025-04-25}}.</ref>.
C'est une cadence conclusive.
==== {{lang|en|Turnaround}} ====
[[Fichier:Au clair de le lune turnaround.midi|thumb|Au clair de la lune, harmonisé en style jazz : accords de 7{{e}}, anatole suivie d'un ''{{lang|en|turnaround}}'' ii-V-I.]]
Le terme ''{{lang|en|turnaround}}'' signifie revirement, retournement. C'est une succession d'accords que fait la transition entre deux parties, en créant une tension-résolution. Le ''{{lang|en|turnaround}}'' le plus courant est la succession {{Times New Roman|II - V - I}}.
On utilise également fréquemment l'anatole : {{Times New Roman|I - VI - II - V}}.
{| class="wikitable"
|+ Progressions typiques d'accords dans une tonalité majeure
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Cadence<br />parfaite<br /> {{Times New Roman|V - I}}
! scope="col" | Cadence<br />italienne<br />{{Times New Roman|IV - V - I}}
! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou IV - V}}
! scope="col" | Cadence<br />plagale<br />{{Times New Roman|IV - I}}
! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}}
! scope="col" | Anatole<br />{{Times New Roman|I - vi - ii - V}}
|-
|''Do'' majeur || || G - C || F - G - C || Dm - G ou F - G || F - C || Dm - G - C || C - Am - Dm - G
|-
|''Sol'' majeur || ''fa''♯ || D - G || C - D - G || Am - D ou C - D || C - G || Am - D - G || G - Em - Am - D
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || A - D || G - A - D || Em - A ou G - A || G - D || Em - A - D || D - Bm - Em - A
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || E - A || D - E - A || Bm - E ou D - E || D - A || Bm - E - A || A - F♯m - B - E
|-
| ''Fa'' majeur || ''si''♭ || C - F || B♭ - C - F || Gm - C ou B♭ - C || B♭ - F || Gm - C - F || F - Dm - Gm - C
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || F - B♭ || E♭ - F - B♭ || Cm - F ou E♭ - F || E♭ - B♭ || Cm - F - B♭ || B♭ - Gm - Cm - F
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || B♭ - E♭ || A♭ - B♭ - E♭ || Fm - B♭ ou A♭ - B♭ || A♭ - E♭ || Fm - B♭ - E♭ || Gm - Cm - Fm - B♭
|}
{| class="wikitable"
|+ Progressions typiques d'accords dans une tonalité mineure
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Cadence<br />parfaite<br />{{Times New Roman|V - i}}
! scope="col" | Cadence<br />italienne<br />{{Times New Roman|iv - V - i}}
! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou iv - V}}
! scope="col" | Cadence<br />plagale<br />{{Times New Roman|iv - i}}
! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}}
! scope="col" | Anatole<br />{{Times New Roman|i - VI - ii - V}}
|-
| ''La'' mineur<br />harmonique || || E - Am || Dm - E - Am || B° - E ou Dm - E || Dm - Am || B° - E - Am || Am - F - B° - E
|-
| ''Mi'' mineur<br />harmonique || ''fa''♯ || B - Em || Am - B - Em || F♯° - B ou Am - B || Am - Em || F♯° - B - Em || Em - C - F♯° - B
|-
| ''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || F♯ - Bm || Em - F♯ - Bm || C♯° - F♯ ou Em - F♯ || Em - Bm || C♯° - F♯ - Bm || Bm - G - C♯° - F♯
|-
| ''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || C♯ - F♯m || Bm - C♯ - F♯m || G♯° - C♯ ou Bm - C♯ || Bm - F♯m || G♯° - C♯ - F♯m || A+ - D - G♯° - C♯
|-
| ''Ré'' mineur<br />harmonique || ''si''♭ || A - Dm || Gm - A - Dm || E° - A ou Gm - A || Gm - Dm || E° - A - Dm || Dm - B♭ - E° - A
|-
| ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || D - Gm || Cm - D - Gm || A° - D ou Cm - D || Cm - Gm|| A° - D - Gm || Gm - E♭ - A° - D
|-
| ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || G - Cm || Fm - G - Cm || D° - G ou Fm - G || Fm - Dm || D° - G - Cm || Cm - A♭ - D° - G
|}
==== Exemple : ''La Mer'' ====
: {{lien web
| url = https://www.youtube.com/watch?v=PXQh9jTwwoA
| titre = Charles Trenet - La mer (Officiel) [Live Version]
| site = YouTube
| auteur = Charles Trenet
| consulté le = 2020-12-24
}}
Le début de ''La Mer'' (Charles Trenet, 1946) est en ''do'' majeur et est harmonisé par l'anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (C - Am - Dm - G<sup>7</sup>) sur deux mesures, jouée deux fois ({{Times New Roman|1=<nowiki>|I-vi|ii-V</nowiki><sup>7</sup><nowiki>|</nowiki>}} × 2). Viennent des variations avec les progressions {{Times New Roman|I-III-vi-V<sup>7</sup>}} (C - E - Am - G<sup>7</sup>) puis la « progression ’50s » (voir plus bas) {{Times New Roman|I-vi-IV-VI<sup>7</sup>}} (C - Am - F - A<sup>7</sup>, on remarque que {{Times New Roman|IV}}/F est le relatif majeur du {{Times New Roman|ii}}/Dm de l'anatole), jouées chacune une fois sur deux mesure ; puis cette première partie se conclut par une demie cadence {{Times New Roman|ii-V<sup>7</sup>}} sur une mesure puis une dernière anatole sur trois mesures ({{Times New Roman|1=<nowiki>|I-vi|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}}). Cela constitue une première partie « A » sur douze mesures qui se termine par une demi-cadence ({{Times New Roman|ii-V<sup>7</sup>}}) qui appelle une suite. Cette partie A est jouée une deuxième fois mais la fin est modifiée pour la transition : les deux dernières mesures {{Times New Roman|<nowiki>|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}} deviennent {{Times New Roman|<nowiki>|ii-V</nowiki><sup>7</sup><nowiki>|I|</nowiki>}} (|Dm-G7|C|), cette partie « A’ » se conclut donc par une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}).
Le morceau passe ensuite en tonalité de ''mi'' majeur, donc une tierce au dessus de ''do'' majeur, sur six mesures. Cette partie utilise une progression ’50s {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (E - C♯m - A - B<sup>7</sup>), qui est rappelons-le une variation de l'anatole, l'accord {{Times New Roman|ii}} (Fm) étant remplacé par son relatif majeur {{Times New Roman|IV}} (A). Cette anatole modifiée est jouée deux fois puis la partie en ''mi'' majeur se conclut par l'accord parfait {{Times New Roman|I}} joué sur deux mesures (|E|E|), on a donc, avec la mesure précédente, avec une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}).
Suivent ensuite six mesures en ''sol'' majeur, donc à nouveau une tierce au dessus de ''mi'' majeur. Elle comporte une progression {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (G - Em - C - D<sup>7</sup>), donc anatole avec substitution du {{Times New Roman|ii}}/Am par son relatif majeur {{Times New Roman|VI}}/C (progression ’50s), puis une anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (G - Em - Am - D<sup>7</sup>) et deux mesure sur la tonique {{Times New Roman|I-I<sup>7</sup>}} (G - G<sup>7</sup>), formant à nouveau une cadence parfaite. La fin sur un accord de septième, dissonant, appelle une suite.
Cette partie « B » de douze mesures comporte donc deux parties similaires « B1 » et « B2 » qui forment une marche harmonique (montée d'une tierce).
Le morceau se conclut par une reprise de la partie « A’ » et se termine donc par une cadence parfaite.
Nous avons une structure A-A’-B-A’ sur 48 mesures, proche la forme AABA étudiée plus loin.
Donc ''La Mer'' est un morceau structuré autour de l'anatole avec des variations (progression ’50s, substitution du {{Times New Roman|ii}} par son relatif majeur {{Times New Roman|IV}}) et comportant une marche harmonique dans sa troisième partie. Les parties se concluent par des ''{{lang|en|turnarounds}}'' sous la forme d'une cadence parfaite ou, pour la partie A, par une demi-cadence.
{| border="1" rules="rows" frame="hsides"
|+ Structure de ''La Mer''
|- align="center"
|
| colspan="12" | ''do'' majeur
|
|- align="center"
! scope="row" rowspan=2 | A
| colspan="2" | anatole
| colspan="2" | //
| colspan="2" | variation
| colspan="2" | ’50s
| ½ c.
| colspan="3" | anatole
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii}} || <nowiki>|</nowiki> {{Times New Roman|V<sup>7</sup>}} || <nowiki>|</nowiki>
|- align="center"
! scope="row" rowspan="2" | A’
| colspan="2" | anatole
| colspan="2" | //
| colspan="2" | variation
| colspan="2" | ’50s
| ½ c.
| colspan="2" | anatole
| c.p.
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki>
|- align="center"
|
| colspan="6" | B1 : ''mi'' majeur
| colspan="6" background="lightgray" | B2 : ''sol'' majeur
|
|- align="center"
! scope="row" rowspan="2" | B
| colspan="2" | ’50s
| colspan="2" | //
|colspan="2" | c.p.
| colspan="2" | ’50s
| colspan="2" | //
|colspan="2" | c.p.
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I<sup>7</sup>}} || <nowiki>|</nowiki>
|-
! scope="row" | A’
| colspan="12" |
|
|}
=== Progression blues ===
La musique blues est apparue dans les années 1860. Elle est en général bâtie sur une grille d'accords ''({{lang|en|changes}})'' immuable de douze mesures ''({{lang|en|twelve-bar blues}})''. C'est sur cet accompagnement qui se répète que s'ajoute la mélodie — chant et solo. Cette structure est typique du blues et se retrouve dans ses dérivés comme le rock 'n' roll.
Le rythme est toujours un rythme ternaire syncopé ''({{lang|en|shuffle, swing, groove}}, ''notes inégales'')'' : la mesure est à quatre temps, mais la noire est divisée en noire-croche en triolet, ou encore triolet de croche en appuyant la première et la troisième.
La mélodie se construit en général sur une gamme blues de six degrés (gamme pentatonique mineure avec une quarte augmentée), mais bien que la gamme soit mineure, l'harmonie est construite sur la gamme majeure homonyme : un blues en ''fa'' a une mélodie sur la gamme de ''fa'' mineur, mais une harmonie sur la gamme de ''fa'' majeur. La grille d'accord comporte les accords construits sur les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la gamme majeure homonyme. Les accords sont souvent des accords de septième (donc avec une tierce majeure et une septième mineure), il ne s'agit donc pas d'une harmonisation de gamme diatonique (puisque la septième est majeure sur l'accord de tonique).
Par exemple, pour un blues en ''do'' :
* accord parfait de do majeur, C ({{Times New Roman|I}}<sup>er</sup> degré) ;
* accord parfait de fa majeur, F ({{Times New Roman|IV}}<sup>e</sup> degré) ;
* accord parfait de sol majeur, G ({{Times New Roman|V}}<sup>e</sup> degré).
Il existe quelques morceaux harmonisés avec des accords mineurs, comme par exemple ''As the Years Go Passing By'' d'Albert King (Duje Records, 1959).
La progression blues est organisée en trois blocs de quatre mesures ayant les fonctions suivantes (voir ci-dessus ''[[#Harmonie fonctionnelle|Harmonie fonctionnelle]]'') :
* quatre mesures toniques ;
* quatre mesures sous-dominantes ;
* quatre mesures dominantes.
La forme la plus simple, que Jeff Gardner appelle « forme A », est la suivante :
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, forme A
|-
! scope="row" | Tonique
| width="50px" | I
| width="50px" | I
| width="50px" | I
| width="50px" | I
|-
! scope="row" | Sous-domminante
| width="50px" | IV
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
! scope="row" | Dominante
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | V
|}
La progression {{Times New Roman|I-V}} des deux dernières mesures forment le ''{{lang|en|turnaround}}'', la demie cadence qui lance le cycle suivant. Nous présentons ci-dessous un exemple typique de ligne de basse ''({{lang|en|walking bass}})'' pour le ''{{lang|en|turnaround}}'' d'un blues en ''la'' :
[[Fichier:Turnaround classique blues en la.svg|Exemple typique de ligne de basse pour un ''turnaround'' de blues en ''la''.]]
[[Fichier:Blues mi harmonie elementaire.midi|thumb|Blues en ''mi'', harmonisé de manière élémentaire avec une ''{{lang|en|walking bass}}''.]]
Vous pouvez écouter ci-contre une harmonisation typique d'un blues en ''mi''. Les accords sont exécutés par une basse marchante ''({{lang|en|walking bass}})'', qui joue une arpège sur la triade avec l'ajout d'une sixte majeure et d'une septième mineure, et par une guitare qui joue un accord de puissance ''({{lang|en|power chord}})'', qui n'est composé que de la fondamentale et de la quinte juste, avec une sixte en appoggiature.
La forme B s'obtient en changeant la deuxième mesure : on joue un degré {{Times New Roman|IV}} au lieu d'un degré {{Times New Roman|I}}. La progression {{Times New Roman|I-IV}} sur les deux premières mesures est appelé ''{{lang|en|quick change}}''.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, forme B
|-
| width="50px" | I
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
| width="50px" | IV
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | V
|}
Par exemple, ''Sweet Home Chicago'' (Robert Johnson, 1936) est un blues en ''fa'' ; sa grille d'accords, aux variations près, suit une forme B :
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression de ''Sweet Home Chicago''
|-
| width="50px" | F
| width="50px" | B♭
| width="50px" | F
| width="50px" | F
|-
| width="50px" | B♭
| width="50px" | B♭
| width="50px" | F
| width="50px" | F
|-
| width="50px" | C7
| width="50px" | B♭7
| width="50px" | F7
| width="50px" | C7
|}
: Écouter {{lien web
| url =https://www.youtube.com/watch?v=dkftesK2dck
| titre = Robert Johnson "Sweet Home Chicago"
| auteur = Michal Angel
| site = YouTube
| date = 2007-12-09 | consulté le = 2020-12-17
}}
Les formes C et D s'obtiennent à partir des formes A et B en changeant le dernier accord par un accord sur le degré {{Times New Roman|I}}, ce qui forme une cadence plagale.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, formes C et D
|-
| colspan="4" | …
|-
| colspan="4" | …
|-
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|}
L'harmonie peut être enrichie, notamment en jazz. Voici par exemple une grille du blues souvent utilisés en bebop.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Exemple de progression de blues bebop sur une base de forme B
|-
| width="60px" | I<sup>7</sup>
| width="60px" | IV<sup>7</sup>
| width="60px" | I<sup>7</sup>
| width="60px" | V–<sup>7</sup> <nowiki>|</nowiki> I<sup>7</sup>
|-
| width="60px" | IV<sup>7</sup>
| width="60px" | IV<sup>7</sup>
| width="60px" | I<sup>7</sup>
| width="60px" | VI<sup>7 ♯9 ♭13</sup>
|-
| width="60px" | II–<sup>7</sup>
| width="60px" | V<sup>7</sup>
| width="60px" | V<sup>7</sup> <nowiki>|</nowiki> IV<sup>7</sup>
| width="60px" | II–<sup>7</sup> <nowiki>|</nowiki> V<sup>7</sup>
|}
On peut aussi trouver des blues sur huit mesures, sur seize mesures comme ''Watermelon Man'' de Herbie Hancock (album ''Takin' Off'', Blue Note, 1962) ou ''Let's Dance'' de Jim Lee (interprété par Chris Montez, Monogram, 1962)
* {{lien web
|url= https://www.dailymotion.com/video/x5iduwo
|titre=Herbie Hancock - Watermelon Man (1962)
|auteur=theUnforgettablesTv
|site=Dailymotion
|date=2003 |consulté le=2021-02-09
}}
* {{lien web
|url=https://www.youtube.com/watch?v=6JXshurYONc
|titre=Let's Dance
|auteur=Chris Montez
|site=YouTube
|date=2016-08-06 |consulté le=2021-02-09
}}
À l'inverse, certains blues peuvent avoir une structure plus simple que les douze mesure ; par exemple ''Hoochie Coochie Man'' de Willie Dixon (interprété par Muddy Waters sous le titre ''Mannish Boy'', Chicago Blues, 1954) est construit sur un seul accord répété tout le long de la chanson.
* {{lien web
|url=https://www.dailymotion.com/video/x5iduwo
|titre=Muddy Waters - Hoochie Coochie Man
|auteur=Muddy Waters
|site=Dailymotion
|date=2012 | consulté le=2021-02-09
}}
=== Cadence andalouse ===
La cadence andalouse est une progression de quatre accords, descendant par mouvement conjoint :
* en mode de ''mi'' (mode phrygien) : {{Times New Roman|IV}} - {{Times New Roman|III}} - {{Times New Roman|II}} - {{Times New Roman|I}} ;<br />par exemple en ''mi'' phrygien : Am - G - F - E ; en ''do'' phrygien : Fm - E♭ - D♭ - C ;<br />on notera que le degré {{Times New Roman|III}} est diésé dans l'accord final (ou bécarre s'il est bémol dans la tonalité) ;
* en mode mineur : {{Times New Roman|I}} - {{Times New Roman|VII}} - {{Times New Roman|VI}} - {{Times New Roman|V}} ;<br />par exemple en ''la'' mineur : Am - G - F - E ; en ''do'' mineur : Cm - B♭ - A♭ - m ;<br />comme précédemment, on notera que le degré {{Times New Roman|VII}} est diésé dans l'accord final.
=== Progressions selon le cercle des quintes ===
[[Fichier:Cercle quintes degres tonalite majeure.svg|vignette|Cercle des quinte justes (parcouru dans le sens des aiguilles d'une montre) des degrés d'une tonalité majeure.]]
La progression {{Times New Roman|V-I}} est la cadence parfaite, mais on peut aussi l'employer au milieu d'un morceau. Cette progression étant courte, sa répétition crée de la lassitude ; on peut la compléter par d'autres accords séparés d'une quinte juste, en suivant le « cercle des quintes » : {{Times New Roman|I-V-IX}}, la neuvième étant enharmonique de la seconde, on obtient {{Times New Roman|I-V-II}}.
On peut continuer de décrire le cercle des quintes : {{Times New Roman|I-V-II-VI}}, on obtient l'anatole dans le désordre ; on peut à l'inverse étendre les quintes vers la gauche, {{Times New Roman|IV-I-V-II-VI}}.
En musique populaire, on trouve fréquemment une progression fondée sur les accord {{Times New Roman|I}}, {{Times New Roman|IV}}, {{Times New Roman|V}} et {{Times New Roman|VI}}, popularisée dans les années 1950. La « progression années 1950 », « progression ''{{lang|en|fifties ('50)}}'' » ''({{lang|en|'50s progression}})'' est dans l'ordre {{Times New Roman|I-VI-IV-V}}. On trouve aussi cette progression en musique classique. Si la tonalité est majeure, la triade sur la sus-dominante est mineure, les autres sont majeures, on notera donc souvent {{Times New Roman|I-vi-IV-V}}. On peut avoir des permutations circulaires (le dernier accord venant au début, ou vice-versa) : {{Times New Roman|vi-IV-V-I}}, {{Times New Roman|IV-V-I-vi}} et {{Times New Roman|V-I-vi-IV}}.
{| class="wikitable"
|+ Accords selon la tonalité
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" style="font-family:Times New Roman" | I
! scope="col" style="font-family:Times New Roman" | IV
! scope="col" style="font-family:Times New Roman" | V
! scope="col" style="font-family:Times New Roman" | vi
|-
|''Do'' majeur || || C || F || G || Am
|-
|''Sol'' majeur || ''fa''♯ || G || C || D || Em
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || D || G || A || Bm
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A || D || E || F♯m
|-
| ''Fa'' majeur || ''si''♭ || F || B♭ || C || Dm
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭ || E♭ || F || Gm
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭ || A♭ || B♭ || Cm
|}
Par exemple, en tonalité de ''do'' majeur, la progression {{Times New Roman|I-vi-IV-V}} sera C-Am-F-G.
Il existe d'autres progressions utilisant ces accords mais dans un autre ordre, typiquement {{Times New Roman|I–IV–vi–V}} ou une de ses permutations circulaires : {{Times New Roman|IV–vi–V-I}}, {{Times New Roman|vi–V-I-IV}} ou {{Times New Roman|V-I-IV-vi}}. Ou dans un autre ordre.
PV Nova l'illustre dans plusieurs de ses « expériences » dans la version {{Times New Roman|vi-V-IV-I}}, soit Am-G-F-C, ou encore {{Times New Roman|vi-IV-I-V}}, soit Am-F-C-G :
: {{lien web
| url = https://www.youtube.com/watch?v=w08LeZGbXq4
| titre = Expérience n<sup>o</sup> 6 — La Happy Pop
| auteur = PV Nova
| site = YouTube
| date = 2011-08-20 | consulté le = 2020-12-13
}}
et cela devient un gag récurrent avec son « chapeau des accords magiques qu'on nous ressort à toutes les sauces »
: {{lien web
| url = https://www.youtube.com/watch?v=VMY_vc4nZAU
| titre = Expérience n<sup>o</sup> 14 — La Soupe dou Brasil
| auteur = PV Nova
| site = YouTube
| date = 2012-10-03 | consulté le = 2020-12-17
}}
Cette récurrence est également parodiée par le groupe The Axis of Awesome avec ses « chansons à quatre accords » ''({{lang|en|four-chords song}})'', dans une sketch où ils mêlent 47 chansons en utilisant l'ordre {{Times New Roman|I-V-vi-IV}} :
: {{lien web
| url = https://www.youtube.com/watch?v=oOlDewpCfZQ
| titre = 4 Chords | Music Videos | The Axis Of Awesome
| auteur = The Axis of Awesome
| site = YouTube
| date = 2011-07-20 | consulté le = 2020-12-17
}}
{{boîte déroulante/début|titre=Chansons mêlées dans le sketch}}
# Journey : ''Don't Stop Believing'' ;
# James Blunt : ''You're Beautiful'' ;
# Black Eyed Peas : ''Where Is the Love'' ;
# Alphaville : ''Forever Young'' ;
# Jason Mraz : ''I'm Yours'' ;
# Train : ''Hey Soul Sister'' ;
# The Calling : ''Wherever You Will Go'' ;
# Elton John : ''Can You Feel The Love Tonight'' (''Le Roi lion'') ;
# Akon : ''Don't Matter'' ;
# John Denver : ''Take Me Home, Country Roads'' ;
# Lady Gaga : ''Paparazzi'' ;
# U2 : ''With Or Without You'' ;
# The Last Goodnight : ''Pictures of You'' ;
# Maroon Five : ''She Will Be Loved'' ;
# The Beatles : ''Let It Be'' ;
# Bob Marley : ''No Woman No Cry'' ;
# Marcy Playground : ''Sex and Candy'' ;
# Men At Work : ''Land Down Under'' ;
# thème de ''America's Funniest Home Videos'' (équivalent des émissions ''Vidéo Gag'' et ''Drôle de vidéo'') ;
# Jack Johnson : ''Taylor'' ;
# Spice Girls : ''Two Become One'' ;
# A Ha : ''Take On Me'' ;
# Green Day : ''When I Come Around'' ;
# Eagle Eye Cherry : ''Save Tonight'' ;
# Toto : ''Africa'' ;
# Beyonce : ''If I Were A Boy'' ;
# Kelly Clarkson : ''Behind These Hazel Eyes'' ;
# Jason DeRulo : ''In My Head'' ;
# The Smashing Pumpkins : ''Bullet With Butterfly Wings'' ;
# Joan Osborne : ''One Of Us'' ;
# Avril Lavigne : ''Complicated'' ;
# The Offspring : ''Self Esteem'' ;
# The Offspring : ''You're Gonna Go Far Kid'' ;
# Akon : ''Beautiful'' ;
# Timberland featuring OneRepublic : ''Apologize'' ;
# Eminem featuring Rihanna : ''Love the Way You Lie'' ;
# Bon Jovi : ''It's My Life'' ;
# Lady Gaga : ''Pokerface'' ;
# Aqua : ''Barbie Girl'' ;
# Red Hot Chili Peppers : ''Otherside'' ;
# The Gregory Brothers : ''Double Rainbow'' ;
# MGMT : ''Kids'' ;
# Andrea Bocelli : ''Time To Say Goodbye'' ;
# Robert Burns : ''Auld Lang Syne'' ;
# Five for fighting : ''Superman'' ;
# The Axis of Awesome : ''Birdplane'' ;
# Missy Higgins : ''Scar''.
{{boîte déroulante/fin}}
Vous pouvez par exemple jouer les accords C-G-Am-F ({{Times New Roman|I-V-vi-IV}}) et chanter dessus ''{{lang|en|Let It Be}}'' (Paul McCartney, The Beattles, 1970) ou ''Libérée, délivrée'' (Robert Lopez, ''La Reine des neiges'', 2013).
La progression {{Times New Roman|I-V-vi-IV}} est considérée comme « optimiste » tandis que sa variante {{Times New Roman|iv-IV-I-V}} est considérée comme « pessimiste ».
On peut voir la progression {{Times New Roman|I-vi-IV-V}} comme une variante de l'anatole {{Times New Roman|I-vi-ii-V}}, obtenue en remplaçant l'accord de sustonique {{Times New Roman|ii}} par l'accord de sous-dominante {{Times New Roman|IV}} (son relatif majeur, et degré ayant la même fonction).
==== Exemples de progression selon le cercle des quintes en musique classique ====
[[Fichier:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.midi|vignette|Dietrich Buxtehude, Psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]]
Cette progression selon la cercle des quintes, sous la forme {{Times New Roman|I-vi-IV-V}}, apparaît déjà au {{pc|xvii}}<sup>e</sup> siècle dans le psaume 42 ''Quem ad modum desiderat cervis'' (BuxVW92) de Dietrich Buxtehude (1637-1707). Le morceau est en ''fa'' majeur, la progression d'accords est donc F-Dm-B♭-C.
: {{lien web
| url = https://www.youtube.com/watch?v=8FmV9l1RqSg
| titre = D. Buxtehude - Quemadmodum desiderat cervus, BuxWV 92
| auteur = Longobardo
| site = YouTube
| date = 2013-04-06 | consulté la = 2021-01-01
}}
[[File:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.svg|vignette|450x450px|center|Dietrich Buxtehude, psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]]
{{clear}}
[[Fichier:JSBach BWV140 cantate 4 mesures.midi|vignette|J.-S. Bach, cantate BWV140, quatre premières mesures.]]
On la trouve également dans l'ouverture de la cantate ''{{lang|de|Wachet auf, ruft uns die Stimme}}'' de Jean-Sébastien Bach (BWV140, 1731). Le morceau est en ''mi''♭ majeur, la progression d'accords est donc E♭-Cm-A♭<sup>6</sup>-B♭.
[[Fichier:JSBach BWV140 cantate 4 mesures.svg|vignette|center|J.-S. Bach, cantate BWV140, quatre premières mesures.|alt=|517x517px]]
{{clear}}
[[Fichier:Mozart K310 Sonate8 mesures 45 a 49.midi|vignette|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]]
La même progression est utilisée par Mozart, par exemple dans le premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778), la progression d'accords est C-Am-F-G qui correspond à la progression {{Times New Roman|III-i-VI-VII}} de ''la'' mineur, mais à la progression {{Times New Roman|I-vi-IV-V}} de la gamme relative, ''do'' majeur .
[[Fichier:Mozart K310 Sonate8 mesures 45 a 49.svg|vignette|center|500px|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]]
=== Substitution tritonique ===
Un des accords les plus utilisés est donc l'accord de septième de dominante, {{Times New Roman|V<sup>7</sup><sub>+</sub>}} qui contient les degrés {{Times New Roman|V}}, {{Times New Roman|VII}}, {{Times New Roman|II}} ({{Times New Roman|IX}}) et {{Times New Roman|IV}}({{Times New Roman|XI}}) ; par exemple, en tonalité de ''do'' majeur, l'accord de ''sol'' septième (G<sup>7</sup>) contient les notes ''sol''-''si''-''ré''-''fa''. Si l'on prend l'accord dont la fondamentale est trois tons (triton) au-dessus ou en dessous — l'octave contenant six tons, on arrive sur la même note —, {{Times New Roman|♭II<sup>7</sup>}}, ici ''ré''♭ septième (D♭<sup>7</sup>), celui-ci contient les notes ''ré''♭-''fa''-''la''♭-''do''♭, cette dernière note étant l'enharmonique de ''si''. Les deux accords G<sup>7</sup> et D♭<sup>7</sup> ont donc deux notes en commun : le ''fa'' et le ''si''/''do''♭.
Il est donc fréquent en jazz de substituer l'accord {{Times New Roman|V<sup>7</sup><sub>+</sub>}} par l'accord {{Times New Roman|♭II<sup>7</sup>}}. Par exemple, la progression {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|V<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}} devient {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|♭II<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}}. C'est un procédé courant de réharmonisation (le fait de remplacer un accord par un autre dans un morceau existant).
Les six substitutions possibles sont donc : C<sup>7</sup>↔F♯<sup>7</sup> - D♭<sup>7</sup>↔G<sup>7</sup> - D<sup>7</sup>↔A♭<sup>7</sup> - E♭<sup>7</sup>↔A<sup>7</sup> - E<sup>7</sup>↔B♭<sup>7</sup> - F<sup>7</sup>↔B<sup>7</sup>.
[[Fichier:Übermäsiger Terzquartakkord.jpg|vignette|Exemple de cadence parfaite en ''do'' majeur avec substitution tritonique (sixte française).]]
Dans l'accord D♭<sup>7</sup>, si l'on remplace le ''do''♭ par son ''si'' enharmonique, on obtient un accord de sixte augmentée : ''ré''♭-''fa''-''la''♭-''si''. Cet accord est utilisé en musique classique depuis la Renaissance ; on distingue en fait trois accords de sixte augmentée :
* sixte française ''ré''♭-''fa''-''sol''-''si'' ;
* sixte allemande : ''ré''♭-''fa''-''la''♭-''si'' ;
* sixte italienne : ''ré''♭-''fa''-''si''.
Par exemple, le ''Quintuor en ''ut'' majeur'' de Franz Schubert (1828) se termine par une cadence parfaite dont l'accord de dominante est remplacé par une sixte française ''ré''♭-''fa''-''si''-''sol''-''si'' (''ré''♭ aux violoncelles, ''fa'' à l'alto, ''si''-''sol'' aux seconds violons et ''si'' au premier violon).
[[Fichier:Schubert C major Quintet ending.wav|vignette|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]]
[[Fichier:Schubert C major Quintet ending.png|vignette|center|upright=2.5|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]]
=== Autres accords de substitution ===
Substituer un accord consiste à utiliser un accord provenant d'une tonalité étrangère à la tonalité en cours. À la différence d'une modulation, la substitution est très courte et ne donne pas l'impression de changer de tonalité ; on a juste un sentiment « étrange » passager. Un court passage dans une autre tonalité est également appelée « emprunt ».
Nous avons déjà vu plusieurs méthodes de substitution :
* utilisation d'une note étrangère : une note étrangère — note de passage, appoggiature, anticipation, retard… — crée momentanément un accord hors tonalité ; en musique classique, ceci n'est pas considéré comme un accord en propre, mais en jazz, on parle « d'accord de passage » et « d'accord suspendu » ;
* utilisation d'une dominante secondaire : l'accord de dominante secondaire est hors tonalité ; le but ici est de faire une cadence parfaite, mais sur un autre degré que la tonique de la tonalité en cours ;
* la substitution tritonique, vue ci-dessus, pour remplacer un accord de septième de dominante.
Une dernière méthode consiste à remplacer un accord par un accord d'une gamme de même tonique, mais d'un autre mode ; on « emprunte » ''({{lang|en|borrow}})'' l'accord d'un autre mode. Par exemple, substituer un accord de la tonalité de ''do'' majeur par un accord de la tonalité de ''do'' mineur ou de ''do'' mode de ''mi'' (phrygien).
Donc en ''do'' majeur, on peut remplacer un accord de ''ré'' mineur septième (D<sub>m</sub><sup>7</sup>) par un accord de ''ré'' demi-diminué (D<sup>⌀</sup>, D<sub>m</sub><sup>7♭5</sup>) qui est un accord appartenant à la donalité de ''la'' mineur harmonique.
=== Forme AABA ===
La forme AABA est composée de deux progressions de huit mesures, notées A et B ; cela représente trente-deux mesures au total, on parle donc souvent en anglais de la ''{{lang|en|32-bars form}}''. C'est une forme que l'on retrouve dans de nombreuses chanson de comédies musicales de Broadway comme ''Have You Met Miss Jones'' (''{{lang|en|I'd Rather Be Right}}'', 1937), ''{{lang|en|Over the Rainbow}}'' (''Le Magicien d'Oz'', Harold Harlen, 1939), ''{{lang|en|All the Things You Are}}'' (''{{lang|en|Very Warm for may}}'', 1939).
Par exemple, la version de ''{{lang|en|Over the Rainbow}}'' chantée par Judy Garland est en ''la''♭ majeur et la progression d'accords est globalement :
* A (couplet) : A♭-Fm | Cm-A♭ | D♭ | Cm-A♭ | D♭ | D♭-F | B♭-E♭ | A♭
* B (pont) : A♭ | B♭m | Cm | D♭ | A♭ | B♭-G | Cm-G | B♭m-E♭
soit en degrés :
* A : {{Times New Roman|<nowiki>I-vi | iii-I | IV | iii-IV | IV | IV-vi | II-V | I</nowiki>}}
* B : {{Times New Roman|<nowiki>I | ii | iii | IV | I | II-VII | iii-VII | ii-V</nowiki>}}
Par rapport aux paroles de la chanson, on a
* A : couplet 1 ''« {{lang|en|Somewhere […] lullaby}} »'' ;
* A : couplet 2 ''« {{lang|en|Somewhere […] really do come true}} »'' ;
* B : pont ''« {{lang|en|Someday […] you'll find me}} »'' ;
* A : couplet 3 ''« {{lang|en|Somewhere […] oh why can't I?}} »'' ;
: {{lien web
| url = https://www.youtube.com/watch?v=1HRa4X07jdE
| titre = Judy Garland - Over The Rainbow (Subtitles)
| site = YouTube
| auteur = Overtherainbow
| consulté le = 2020-12-17
}}
Une mise en œuvre de la forme AABA couramment utilisée en jazz est la forme anatole (à le pas confondre avec la succession d'accords du même nom), en anglais ''{{lang|en|rythm changes}}'' car elle s'inspire du morceau ''{{lang|en|I Got the Rythm}}'' de George Gerschwin (''Girl Crazy'', 1930) :
* A : {{Times New Roman|I–vi–ii–V}} (succession d'accords « anatole ») ;
* B : {{Times New Roman|III<sup>7</sup>–VI<sup>7</sup>–II<sup>7</sup>–V<sup>7</sup>}} (les fondamentales forment une succession de quartes, donc parcourent le « cercle des quintes » à l'envers).
Par exemple, ''I Got the Rythm'' étant en ''ré''♭ majeur, la forme est :
* A : D♭ - B♭m - E♭m - A♭
* B : F7 - B♭7 - E♭7 - A♭7
=== Exemples ===
==== Début du Largo de la symphonie du Nouveau Monde ====
[[File:Largo nouveau monde 5 1res mesures.svg|vignette|Partition avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]]
[[File:Largo nouveau monde 5 1res mesures.midi|vignette|Fichier son avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]]
Nous avons reproduit ci-contre les cinq premières mesure du deuxième mouvement Largo de la symphonie « Du Nouveau Monde » (symphonie n<sup>o</sup> 9 d'Antonín Dvořák, 1893). Cliquez sur l'image pour l'agrandir.
Vous pouvez écouter cette partie jouée par un orchestre symphonique :
* {{lien web
|url =https://www.youtube.com/watch?v=y2Nw9r-F_yQ?t=565
|titre = Dvorak Symphony No.9 "From the New World" Karajan 1966
|site=YouTube (Seokjin Yoon)
|consulté le=2020-12-11
}} (à 9 min 25), par le Berliner Philharmoniker, dirigé par Herbert von Karajan (1966) ;
* {{lien web
|url = https://www.youtube.com/watch?v=ASlch7R1Zvo
|titre=Dvořák: Symphony №9, "From The New World" - II - Largo
|site=YouTube (diesillamusicae)
|consulté le=2020-12-11
}} : Wiener Philharmoniker, dirigé par Herbert von Karajan (1985).
{{clear}}
Cette partie fait intervenir onze instruments monodiques (ne jouant qu'une note à la fois) : des vents (trois bois, sept cuivres) et une percussion. Certains de ces instruments sont transpositeurs (les notes sur la partition ne sont pas les notes entendues). Jouées ensemble, ces onze lignes mélodiques forment des accords.
Pour étudier cette partition, nous réécrivons les parties des instruments transpositeurs en ''do'' et les parties en clef d’''ut'' en clef de ''fa''. Nous regroupons les parties en clef de ''fa'' d'un côté et les parties en clef de ''sol'' d'un autre.
{{boîte déroulante|Résultat|contenu=[[File:Largo nouveau monde 5 1res mesures transpositeurs en do.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde, en do.]]}}
Nous pouvons alors tout regrouper sous la forme d'un système de deux portées clef de ''fa'' et clef de ''sol'', comme une partition de piano.
{{boîte déroulante|Résultat|contenu=
[[Fichier:Largo nouveau monde 5 1res mesures accords.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords.]]
}}
[[Fichier:Largo nouveau monde 5 1res mesures accords.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords.]]
{{clear}}
Ensuite, nous ne gardons que la basse et les notes médium. Nous changeons éventuellement certaines notes d'octave afin de n'avoir que des superpositions de tierce ou de quinte (état fondamental des accords, en faisant ressortir les notes manquantes).
{{boîte déroulante|Résultat|contenu=
[[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords simplifiés.]]
}}
[[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords simplifiés.]]
Vous pouvez écouter cette partie jouée par un quintuor de cuivres (trompette, bugle, cor, trombone, tuba), donc avec des accords de cinq notes :
: {{lien web
|url=https://www.youtube.com/watch?v=pWfe60nbvjA
|titre = Largo from The New World Symphony by Dvorak
|site=YouTube (The Chamberlain Brass)
|consulté le=2020-12-11
}} : The American Academy of Arts & Letters in New York City (2017).
Nous allons maintenant chiffrer les accords.
Pour établir la basse chiffrée, il nous faut déterminer le parcours harmonique. Pour le premier accord, les tonalités les plus simples avec un ''sol'' dièse sont ''la'' majeur et ''fa'' dièse mineur ; comme le ''mi'' est bécarre, nous retenons ''la'' majeur, il s'agit donc d'un accord de quinte sur la dominante (les accords de dominante étant très utilisés, cela nous conforte dans notre choix). Puis nous avons un ''si'' bémol, nous pouvons être en ''fa'' majeur ou en ''ré'' mineur ; nous retenons ''fa'' majeur, c'est donc le renversement d'un accord sur le degré {{Times New Roman|II}}.
Dans la deuxième mesure, nous revenons en ''la'' majeur, puis, avec un ''la'' et un ''ré'' bémols, nous sommes en ''la'' bémol majeur ; nous avons donc un accord de neuvième incomplet sur la sensible, ou un accord de onzième incomplet sur la dominante.
Dans la troisième mesure, nous passons en ''ré'' majeur, avec un accord de dominante. Puis, nous arrivons dans la tonalité principale, avec le renversement d'un accord de dominante sans tierce suivi d'un accord de tonique. Nous avons donc une cadence parfaite, conclusion logique d'une phrase.
La progression des accords est donc :
{| class="wikitable"
! scope="row" | Tonalité
| ''la'' M - ''fa'' M || ''la'' M - ''la''♭ M || ''ré'' M - ''ré''♭ M || ''ré''♭ M
|-
! scope="row" | Accords
| {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|II}}<sup>6</sup><sub>4</sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|“V”}}<sup>9</sup><sub><s>5</s></sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|V}}<sup>+4</sup> || {{Times New Roman|I}}<sup>5</sup>
|}
Dans le chiffrage jazz, nous avons donc :
* une triade de ''mi'' majeur, E ;
* une triade de ''sol'' majeur avec un ''ré'' en basse : G/D ;
* à nouveau un E ;
* un accord de ''sol'' neuvième diminué incomplet, avec un ''ré'' bémol en basse : G dim<sup>9</sup>/D♭ ;
* un accord de ''la'' majeur, A ;
* un accord de ''la'' bémol septième avec une ''sol'' bémol à la basse : A♭<sup>7</sup>/G♭ ;
* la partie se conclue par un accord parfait de ''ré''♭ majeur, D♭.
Soit une progression E - G/D | E - G dim<sup>9</sup>/D♭ | A - A♭<sup>7</sup>/G♭ | D♭.
[[Fichier:Largo nouveau monde 5 1res mesures accords chiffres.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde en accords simplifiés.]]
{{clear}}
==== Thème de Smoke on the Water ====
Le morceau ''Smoke on the Water'' du groupe Deep Purple (album ''Machine Head'', 1972) possède un célèbre thème, un riff ''({{lang|en|rythmic figure}})'', joué à la guitare sous forme d'accords de puissance ''({{lang|en|power chords}})'', c'est-à-dire des accords sans tierce. Le morceau est en tonalité de ''sol'' mineur naturel (donc avec un ''fa''♮) avec ajout de la note bleue (''{{lang|en|blue note}}'', quinte diminuée, ''ré''♭), et les accords composant le thème sont G<sup>5</sup>, B♭<sup>5</sup>, C<sup>5</sup> et D♭<sup>5</sup>, ce dernier accord étant l'accord sur la note bleue et pouvant être considéré comme une appoggiature (indiqué entre parenthèse ci-après). On a donc ''a priori'', sur les deux premières mesures, une progression {{Times New Roman|I-III-IV}} puis {{Times New Roman|I-III-(♭V)-IV}}. Durant la majeure partie du thème, la guitare basse tient la note ''sol'' en pédale.
{{note|En jazz, la qualité « <sup>5</sup> » indique que l'on n'a que la quinte (et donc pas la tierce), contrairement à la notation de basse chiffrée.}}
: {{lien web
| url = https://www.dailymotion.com/video/x5ili04
| titre = Deep Purple — Smoke on the Water (Live at Montreux 2006)
| auteur = Deep Purple
| site = Dailymotion
| date = 2016 | consulté le = 2020-12-31
}}
Cependant, cette progression forme une mélodie, on peut donc plus la voir comme un contrepoint, la superposition de deux voies ayant un mouvement conjoint, joué par un seul instrument, la guitare, la voie 2 étant jouée une quarte juste en dessous de la voie 1 (la quarte juste descendante étant le renversement de la quinte juste ascendante) :
* voie 1 (aigu) : | ''sol'' - ''si''♭ - ''do'' | ''sol'' - ''si''♭ - (''ré''♭) - ''do'' | ;
* voie 2 (grave) : | ''ré'' - ''fa'' - ''sol'' | ''ré'' - ''fa'' - (''la''♭) - ''sol'' |.
En se basant sur la basse (''sol'' en pédale), nous pouvons considérer que ces deux mesures sont accompagnées d'un accord de Gm<sup>7</sup> (''sol''-''si''♭-''ré''-''fa''), chaque accord de la mélodie comprenant à chaque fois au moins une note de cet accord à l'exception de l'appogiature.
{| class="wikitable"
|+ Mise en évidence des notes de l'accord Gm<sup>7</sup>
|-
! scope="row" | Accords
| G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup>
|-
! scope="row" | Voie 1
| '''''sol''''' || '''''si''♭''' || ''do''
|-
! scope="row" | Voie 2
| '''''ré''''' || '''''fa''''' || '''''sol'''''
|-
! scope="row" | Basse
| '''''sol''''' || '''''sol''''' || '''''sol'''''
|}
Sur les deux mesures suivantes, la basse varie et suit les accords de la guitare avec un retard sur le dernier accord :
{| class="wikitable"
|+ Voies sur les mesure 3-4 du thème
|-
! scope="row" | Accords
| G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> || B♭<sup>5</sup> || G<sup>5</sup>
|-
! scope="row" | Voie 1
| ''sol'' || ''si''♭ || ''do'' || ''si''♭ || ''sol''
|-
! scope="row" | Voie 2
| ''ré'' || ''fa'' || ''sol'' || ''fa'' || ''ré''
|-
! scope="row" | Basse
| ''sol'' || ''sol'' || ''do'' || ''si''♭ || ''si''♭-''sol''
|}
Le couplet de cette chanson est aussi organisé sur une progression de quatre mesures, la guitare faisant des arpèges sur les accords G<sup>5</sup> (''sol''-''ré''-''sol'') et F<sup>5</sup> (''fa''-''do''-''fa'') :
: | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-F<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> |
soit une progression {{Times New Roman|<nowiki>| I-I | I-I | I-VII | I-I |</nowiki>}}. Nous pouvons aussi harmoniser le riff du thème sur cette progression, avec un accord F (''fa''-''la''-''do'') ; nous pouvons aussi nous rappeler que l'accord sur le degré {{Times New Roman|VII}} est plus volontiers considéré comme un accord de septième de dominante {{Times New Roman|V<sup>7</sup>}}, soit ici un accord Dm<sup>7</sup> (''ré''-''fa''-''la''-''do''). On peut donc considérer la progression harmonique sur le thème :
: | Gm-Gm | Gm-Gm | Gm-F ou Dm<sup>7</sup> | Gm-Gm |.
Cette analyse permet de proposer une harmonisation enrichie du morceau, tout en se rappelant qu'une des forces du morceau initial est justement la simplicité de sa structure, qui fait ressortir la virtuosité des musiciens. Nous pouvons ainsi comparer la version album à la version concert avec orchestre ou à la version latino de Pat Boone. À l'inverse, le groupe Psychostrip, dans une version grunge, a remplacé les accords par une ligne mélodique :
* le thème ne contient plus qu'une seule voie (la guitare ne joue pas des accords de puissance) ;
* dans les mesures 9 et 10, la deuxième guitare joue en contrepoint de type mouvement inverse, qui est en fait la voie 2 jouée en miroir ;
* l'arpège sur le couplet est remplacé par une ligne mélodique en ostinato sur une gamme blues.
{| class="wikitable"
|+ Contrepoint sur les mesures 9 et 10
|-
! scope="row" | Guitare 1
| ''sol'' ↗ ''si''♭ ↗ ''do''
|-
! scope="row" | Guitare 2
| ''sol'' ↘ ''fa'' ↘ ''ré''
|}
* {{lien web
| url = https://www.dailymotion.com/video/x5ik234
| titre = Deep Purple — Smoke on the Water (In Concert with the London Symphony Orchestra, 1999)
| auteur = Deep Purple
| site = Dailymotion
| date = 2016 | consulté le = 2020-12-31
}}
* {{lien web
| url = https://www.youtube.com/watch?v=MtUuNzVROIg
| titre = Pat Boone — Smoke on the Water (In a Metal Mood, No More Mr. Nice Guy, 1997)
| auteur = Orrore a 33 Giri
| site = YouTube
| date = 2019-06-24 | consulté le = 2020-12-31
}}
* {{lien web
| url = https://www.youtube.com/watch?v=n7zLlZ8B0Bk
| titre = Smoke on the Water (Heroes, 1993)
| auteur = Psychostrip
| site = YouTube
| date = 2018-06-20 | consulté le = 2020-12-31
}}
== Accords et improvisation ==
Nous avons vu précédemment (chapitre ''[[../Gammes et intervalles#Modes et improvisation|Gammes et intervalles > Modes et improvisation]]'') que le choix d'un mode adapté permet d'improviser sur un accord. L'harmonisation des gammes permet, en inversant le processus, d'étendre notre palette : il suffit de repérer l'accord sur une harmonisaiton de gamme, et d'utiliser cette gamme-là, dans le mode correspondant du degré de l'accord (voir ci-dessus ''[[#Harmonisation par des accords de septième|Harmonisation par des accords de septième]]'').
Par exemple, nous avons vu que l'accord sur le septième degré d'une gamme majeure était un accord demi-diminué ; nous savons donc que sur un accord demi-diminué, nous pouvons improviser sur le mode correspondant au septième degré, soit le mode de ''si'' (locrien).
Un accord de septième de dominante étant commun aux deux tonalités homonymes (par exemple ''fa'' majeur et ''fa'' mineur pour un ''do''<sup>7</sup><sub>+</sub> / C<sup>7</sup>), nous pouvons utiliser le mode de ''sol'' de la gamme majeure (mixolydien) ou de la gamme mineure mineure (mode phrygien dominant, ou phrygien espagnol) pour improviser. Mais l'accord de septième de dominante est aussi l'accord au début d'une grille blues ; on peut donc improviser avec une gamme blues, même si la tierce est majeure dans l'accord et mineure dans la gamme.
[[Fichier:Mode improvisation accords do complet.svg]]
== Autres accords courants ==
[[fichier:Cluster cdefg.png|vignette|Agrégat ''do - ré - mi - fa - sol''.]]
Nous avons vu précédemment l'harmonisation des tonalités majeures et mineures harmoniques par des triades et des accords de septième ; certains accords étant rarement utilisés (l'accord sur le degré {{Times New Roman|III}} et, pour les tonalités mineures harmoniques, l'accord sur la tonique), certains accords étant utilisés comme des accords sur un autre degré (les accords sur la sensible étant considérés comme des accords de dominante sans fondamentale).
Dans l'absolu, on peut utiliser n'importe quelle combinaison de notes, jusqu'aux agrégats, ou ''{{lang|en|clusters}}'' (mot anglais signifiant « amas », « grappe ») : un ensemble de notes contigües, séparées par des intervalles de seconde. Dans la pratique, on reste souvent sur des accords composés de superpositions de tierces, sauf dans le cas de transitions (voir la section ''[[#Notes étrangères|Notes étrangère]]'').
=== En musique classique ===
On utilise parfois des accords dont les notes ne sont pas dans la tonalité (hors modulation). Il peut s'agir d'accords de passage, de notes étrangères, par exemple utilisant un chromatisme (mouvement conjoint par demi-tons).
Outre les accords de passage, les autres accords que l'on rencontre couramment en musique classique sont les accords de neuvième, et les accords de onzième et treizième sur tonique. Ces accords sont simplement obtenus en continuant à empiler les tierces. Il n'y a pas d'accord d'ordre supérieur car la quinzième est deux octaves au-dessus de la fondamentale.
Comme pour les accords de septième, on distingue les accords de neuvième de dominante et les accords de neuvième d'espèce. Dans le cas de la neuvième de dominante, il y a une différence entre les tonalités majeures et mineures : l'intervalle de neuvième est respectivement majeur et mineur. Les chiffrages des renversements peuvent donc différer. Comme pour les accords de septième de dominante, on considère que les accords de septième sur le degré {{Times New Roman|VI}} sont en fait des accords de neuvième de dominante sans fondamentale.
Les accords de neuvième d'espèce sont en général préparés et résolus. Préparés : la neuvième étant une note dissonante (c'est à une octave près la seconde de la fondamentale), l'accord qui précède doit contenir cette note, mais dans un accord consonant ; la neuvième est donc commune avec l'accord précédent. Résolus : la dissonance est résolue en abaissant la neuvième par un mouvement conjoint. Par exemple, en tonalité de ''do'' majeur, si l'on veut utiliser un accord de neuvième d'espèce sur la tonique ''(do - mi - sol - si - ré)'', on peut utiliser avant un accord de dominante ''(sol - si - ré)'' en préparation puis un accord parfait sur le degré {{Times New Roman|IV}} ''(fa - la - do)'' en résolution ; nous avons donc sur la voie la plus aigüe la succession ''ré'' (consonant) - ''ré'' (dissonant) - ''do'' (consonant).
On rencontre également parfois des accords de onzième et de treizième. On omet en général la tierce, car elle est dissonante avec la onzième. L'accord le plus fréquemment rencontré est l'accord sur la tonique : on considère alors que c'est un accord sur la dominante que l'on a enrichi « par le bas », en ajoutant une quinte inférieure. par exemple, dans la tonalité de ''do'' majeur, l'accord ''do - sol - si - ré - fa'' est considéré comme un accord de septième de dominante sur tonique, le degré étant noté « {{Times New Roman|V}}/{{Times New Roman|I}} ». De même pour l'accord ''do - sol - si - ré - fa - la'' qui est considéré comme un accord de neuvième de dominante sur tonique.
=== En jazz ===
En jazz, on utilise fréquemment l'accord de sixte à la place de l'accord de septième majeure sur la tonique. Par exemple, en ''do'' majeur, on utilise l'accord C<sup>6</sup> ''(do - mi - sol - la)'' à la place de C<sup>Δ</sup> ''(do - mi - sol - si)''. On peut noter que C<sup>6</sup> est un renversement de Am<sup>7</sup> et pourrait donc se noter Am<sup>7</sup>/C ; cependant, le fait de le noter C<sup>6</sup> indique que l'on a bien un accord sur la tonique qui s'inscrit dans la tonalité de ''do'' majeur (et non, par exemple, de ''la'' mineur naturelle) — par rapport à l'harmonie fonctionnelle, on remarquera que Am<sup>7</sup> a une fonction tonique, l'utilisation d'un renversement de Am<sup>7</sup> à la place d'un accord de C<sup>Δ</sup> est donc logique.
Les accords de neuvième, onzième et treizième sont utilisés comme accords de septième enrichis. Le chiffrage suit les règles habituelles : on ajoute un « 9 », un « 11 » ou un « 13 » au chiffrage de l'accord de septième.
On utilise également des accords dits « suspendus » : ce sont des accords de transition qui sont obtenus en prenant une triade majeure ou mineure et en remplaçant la tierce par la quarte juste (cas le plus fréquent) ou la seconde majeure. Plus particulièrement, lorsque l'on parle simplement « d'accord suspendu » sans plus de précision, cela désigne l'accord de neuvième avec une quarte suspendue, noté « 9sus4 » ou simplement « sus ».
== L'harmonie tonale ==
L'harmonie tonale est un ensemble de règle assez strictes qui s'appliquent dans la musique savante européenne, de la période baroque à la période classique classique ({{pc|xiv}}<sup>e</sup>-{{pc|xviii}}<sup>e</sup> siècle). Certaines règles sont encore largement appliquées dans divers styles musicaux actuels, y compris populaire (rock, rap…), d'autres sont au contraire ignorées (par exemple, un enchaînement de plusieurs accords de même qualité forme un mouvement parallèle, ce qui est proscrit en harmonie tonale). De nos jours, on peut voir ces règles comme des règles « de bon goût », et leur application stricte comme une manière de composer « à la manière de ».
Précédemment, nous avons vu la progression des accords. Ci-après, nous abordons aussi la manière dont les notes de l'accord sont réparties entre plusieurs voix, et comment on construit chaque voix.
=== Concepts fondamentaux ===
; Consonance
: Les intervalles sont considérés comme « plus ou moins consonants » :
:* consonance parfaite : unisson, quinte et octave ;
:* consonance mixte (parfaite dans certains contextes, imparfaite dans d'autres) : quarte ;
:* consonance imparfaite : tierce et sixte ;
:* dissonance : seconde et septième.
; Degrés
: Certains degrés sont considérés comme « forts », « meilleurs », ce sont les « notes tonales » : {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (sous-dominante) et {{Times New Roman|V}} (dominante).
[[Fichier:Mouvements harmoniques.svg|vignette|upright=0.75|Mouvements harmoniques.]]
; Mouvements
: Le mouvement décrit la manière dont les voix évoluent les unes par rapport aux autres :
:# Mouvement parallèle : les voix sont séparées par un intervalle constant.
:# Mouvement oblique : une voix reste constante, c'est le bourdon ; l'autre monte ou descend.
:# Mouvement contraire : une voix descend, l'autre monte.
:# Échange de voix : les voix échangent de note ; les mélodies se croisent mais on a toujours le même intervalle harmonique.
{{clear}}
=== Premières règles ===
; Règle du plus court chemin
: Quand on passe d'un accord à l'autre, la répartition des notes se fait de sorte que chaque voix fait le plus petit mouvement possible. Notamment : si les deux accords ont des notes en commun, alors les voix concernées gardent la même note.
: Les deux voix les plus importantes sont la voix aigüe — soprano — et la voix la plus grave — basse. Ces deux voix sont relativement libres : la voix de soprano a la mélodie, la voix de basse fonde l'harmonie. La règle du plus court chemin s'applique surtout aux voix intermédiaires ; si l'on a des mouvements conjoints, ou du moins de petits intervalles — c'est le sens de la règle du plus court chemin —, alors les voix sont plus faciles à interpréter. Cette règle évite également que les voix n'empiètent l'une sur l'autre (voir la règle « éviter le croisement des voix »).
; Éviter les consonances parfaites consécutives
:* Lorsque deux voix sont à l'unisson ou à l'octave, elles ne doivent pas garder le même intervalle, l'effet serait trop plat.
:* Lorsque deux voix sont à la quarte ou à la quinte, elles ne doivent pas garder le même intervalle, car l'effet est trop dur.
: Pour éviter cela, lorsque l'on part d'un intervalle juste, on a intérêt à pratiquer un mouvement contraire aux voix qui ne gardent pas la même note, ou au moins un mouvement direct : les voix vont dans le même sens, mais l'intervalle change.
: Notez que même avec le mouvement contraire, on peut avoir des consonances parfaites consécutives, par exemple si une voix fait ''do'' aigu ↗ ''sol'' aigu et l'autre ''sol'' médium ↘ ''do'' grave.
: L'interdiction des consonances parfaites consécutives n'a pas été toujours appliquée, le mouvement parallèle strict a d'ailleurs été le premier procédé utilisé dans la musique religieuse au {{pc|x}}<sup>e</sup> siècle. On peut par exemple utiliser des quintes parallèles pour donner un style médiéval au morceau. On peut également utiliser des octaves parallèles sur plusieurs notes afin de créer un effet de renforcement de la mélodie.
: Par ailleurs, les consonances parfaites consécutives sont acceptées lorsqu'il s'agit d'une cadence (transition entre deux parties ou bien conclusion du morceau).
; Éviter le croisement des voix
: Les voix sont organisées de la plus grave à la plus aigüe. Deux voix n'étant pas à l'unisson, celle qui est plus aigüe ne doit pas devenir la plus grave et ''vice versa''.
; Soigner la partie soprano
: Comme c'est celle qu'on entend le mieux, c'est en général celle qui porte la mélodie principale. On lui applique des règles spécifiques :
:# Si elle chante la sensible dans un accord de dominante ({{Times New Roman|V}}), alors elle doit monter à la tonique, c'est-à-dire que la note suivante sera la tonique située un demi-ton au dessus.
:# Si l'on arrive à une quinte ou une octave entre les parties basse et soprano par un mouvement direct, alors sur la partie soprano, le mouvement doit être conjoint. On doit donc arriver à cette situation par des notes voisines au soprano.
; Préférer certains accords
: Les deux degrés les plus importants sont la tonique ({{Times New Roman|I}}) et la dominante ({{Times New Roman|V}}), les accords correspondants ont donc une importance particulière.
: À l'inverse, l'accord de sensible ({{Times New Roman|VII}}) n'est pas considéré comme ayant une fonction harmonique forte. On le considère comme un accord de dominante affaibli. En tonalité mineure, on évite également l'accord de médiante ({{Times New Roman|III}}).
: Donc on utilise en priorité les accords de :
:# {{Times New Roman|I}} et {{Times New Roman|V}}.
:# Puis {{Times New Roman|II}}, {{Times New Roman|IV}}, {{Times New Roman|VI}} ; et {{Times New Roman|III}} en mode majeur.
:# On évite {{Times New Roman|VII}} ; et {{Times New Roman|III}} en mode mineur.
; Préférer certains enchaînements
: Les enchaînements d'accord peuvent être classés par ordre de préférence. Par ordre de préférence décroissante (du « meilleur » au « moins bon ») :
:# Meilleurs enchaînements : quarte ascendante ou descendante. Notons que la quarte est le renversement de la quinte, on a donc des enchaînements stables et naturels, mais avec un intervalle plus court qu'un enchaînement de quintes.
:# Bons enchaînements : tierce ascendante ou descendante. Les accords consécutifs ont deux notes en commun.
:# Enchaînements médiocres : seconde ascendante ou descendante. Les accords sont voisins, mais ils n'ont aucune note en commun. On les utilise de préférence en mouvement ascendant, et on utilise surtout les enchaînements {{Times New Roman|IV}}-{{Times New Roman|V}}, {{Times New Roman|V}}-{{Times New Roman|VI}} et éventuellement {{Times New Roman|I}}-{{Times New Roman|II}}.
:# Les autres enchaînements sont à éviter.
: On peut atténuer l'effet d'un enchaînement médiocre en plaçant le second accord sur un temps faible ou bien en passant par un accord intermédiaire.
[[Fichier:Progression Vplus4 I6.svg|thumb|Résolution d'un accord de triton (quarte sensible) vers l'accord de sixte de la tonique.]]
; La septième descend par mouvement conjoint
: Dans un accord de septième de dominante, la septième — qui est donc le degré {{Times New Roman|IV}} — descend par mouvement conjoint — elle est donc suivie du degré {{Times New Roman|III}}.
: Corolaire : un accord {{Times New Roman|V}}<sup>+4</sup> se résout par un accord {{Times New Roman|I}}<sup>6</sup> : on a bien un enchaînement {{Times New Roman|V}} → {{Times New Roman|I}}, et la 7{{e}} (degré {{Times New Roman|IV}}), qui est la basse de l'accord {{Times New Roman|V}}<sup>+4</sup>, descend d'un degré pour donner la basse de l'accord {{Times New Roman|I}}<sup>6</sup> (degré {{Times New Roman|III}}).
{{clear}}
[[Fichier:Progression I64 V7plus I5.svg|thumb|Accord de sixte et de quarte cadentiel.]]
; Un accord de sixte et quarte est un accord de passage
: Le second renversement d'un accord parfait est soit une appoggiature, soit un accord de passage, soit un accord de broderie.
: S'il s'agit de l'accord de tonique {{Times New Roman|I}}<sup>6</sup><sub>4</sub>, c'est « accord de sixte et quarte de cadence », l'appoggiature de l'accord de dominante de la cadence parfaite.
{{clear}}
Mais il faut appliquer ces règles avec discernement. Par exemple, la voix la plus aigüe est celle qui s'entend le mieux, c'est donc elle qui porte la mélodie principale. Il est important qu'elle reste la plus aigüe. La voix la plus grave porte l'harmonie, elle pose les accords, il est donc également important qu'elle reste la plus grave. Ceci a deux conséquences :
# Ces deux voix extrêmes peuvent avoir des intervalles mélodiques importants et donc déroger à la règle du plus court chemin : la voix aigüe parce que la mélodie prime, la voix de basse parce que la progression d'accords prime.
# Les croisements des voix intermédiaires sont moins critiques.
Par ailleurs, si l'on applique strictement toutes les règles « meilleurs accords, meilleurs enchaînements », on produit un effet conventionnel, stéréotypé. Il est donc important d'utiliser les solutions « moins bonnes », « médiocres » pour apporter de la variété.
Ajoutons que les renversements d'accords permettent d'avoir plus de souplesse : on reste sur le même accord, mais on enrichit la mélodie sur chaque voix.
Le ''Bolero'' de Maurice Ravel (1928) brise un certain nombre de ces règles. Par exemple, de la mesure 39 à la mesure 59, la harpe joue des secondes. De la mesure 149 à la mesure 165, les piccolo jouent à la sixte, dans des mouvement strictement parallèle, ce qui donne d'ailleurs une sonorité étrange. À partir de la mesure 239, de nombreux instruments jouent en mouvement parallèles (piccolos, flûtes, hautbois, cor, clarinettes et violons).
=== Application ===
[[Fichier:Harmonisation possible de frere jacques exercice.svg|vignette|Exercice : harmoniser ''Frère Jacques''.]]
Harmoniser ''Frère Jacques''.
Nous considérons un morceau à quatre voix : basse, ténor, alto et soprano. La soprano chante la mélodie de ''Frère Jacques''. L'exercice consiste à proposer l'écriture des trois autres voix en respectant les règles énoncées ci-dessus. Pour simplifier, nous ajoutons les contraintes suivantes :
* toutes les voix chantent des blanches ;
* nous nous limitons aux accords de quinte (accords de trois sons composés d'une tierce et d'une quinte) sans avoir recours à leurs renversements (accords de sixte, accords de sixte et de quarte).
Les notes à gauche de la portée indiquent la tessiture (ou ambitus), l'amplitude que peut chanter la voix.
{{clear}}
{{boîte déroulante/début|titre=Solution possible}}
[[Fichier:Harmonisation possible de frere jacques solution.svg|vignette|Harmonisation possible de ''Frère Jacques'' (solution de l'exercice).]]
Il n'y a pas qu'une solution possible.
Le premier accord doit contenir un ''do''. Nous sommes manifestement en tonalité de ''do'' majeur, nous proposons de commencer par l'accord parfait de ''do'' majeur, I<sup>5</sup>.
Le deuxième accord doit comporter un ''ré''. Si nous utilisons l'accord de quinte de ''ré'', nous allons créer une quinte parallèle. Nous pourrions utiliser un renversement, mais nous nous imposons de chercher un autre accord. Il peut s'agir de l'accord ''si''<sup>5</sup> ''(si-ré-fa)'' ou de l'accord de ''sol''<sup>5</sup> ''(sol-si-ré)''. La dernière solution permet d'utiliser l'accord de dominante qui est un accord important de la tonalité. La règle du plus court chemin imposerait le ''sol'' grave pour la partie de basse, mais cela est proche de la limite du chanteur, nous préférons passer au ''sol'' aigu, plus facile à chanter. Nous vérifions qu'il n'y a pas de quinte parallèle : l'intervalle ascendant ''do-sol'' (basse-alto) devient ''sol-si'' (3<sup>ce</sup>), l'intervalle descendant ''do-sol'' (soprano-alto) devient ''ré-si'' (3<sup>ce</sup>).
De la même manière, pour le troisième accord, nous ne pouvons pas passer à un accord de ''la''<sup>5</sup> pour éviter une quinte parallèle. Nous avons le choix entre ''do''<sup>5</sup> ''(do-mi-sol)'' et ''mi''<sup>5</sup> ''(mi-sol-si)''. Nous préférons revenir à l'accord de fondamental, solution très stable (l'enchaînement {{Times New Roman|V}}-{{Times New Roman|I}} formant une cadence parfaite).
Pour le quatrième accord, nous pourrions rester sur l'accord parfait de ''do'' mais cela planterait en quelque sorte la fin du morceau puisque l'on resterait sur la cadence parfaite ; or, nous connaissons le morceau et savons qu'il n'est pas fini. Nous choisissons l'accord de ''la''<sup>5</sup> qui est une sixte ascendante ({{Times New Roman|I}}-{{Times New Roman|VI}}).
Nos aurions pu répartir les voix différemment. Par exemple :
* alto : ''sol''-''si''-''sol''-''do'' ;
* ténor : ''mi''-''ré''-''mi''-''mi''.
{{boîte déroulante/fin}}
[[Fichier:Harmonisation possible de frere jacques.midi|vignette|Fichier son correspondant.]]
{{clear}}
== Annexe ==
=== Accords en musique classique ===
Un accord est un ensemble de notes jouées simultanément. Il peut s'agir :
* de notes jouées par plusieurs instruments ;
* de notes jouées par un même instrument : piano, clavecin, orgue, guitare, harpe (la plupart des instruments à clavier et des instruments à corde).
Pour deux notes jouées simultanément, on parle d'intervalle « harmonique » (par opposition à l'intervalle « mélodique » qui concerne les notes jouées successivement).
Les notes répétées à différentes octaves ne changent pas la nature de l'accord.
La musique classique considère en général des empilements de tierces ; un accord de trois notes sera constitué de deux tierces successives, un accord de quatre notes de trois tierces…
Lorsque tous les intervalles sont des intervalles impairs — tierces, quintes, septièmes, neuvièmes, onzièmes, treizièmes… — alors l'accord est dit « à l'état fondamental » (ou encore « primitif » ou « direct »). La note de la plus grave est appelée « fondamentale » de l'accord. Lorsque l'accord comporte un ou des intervalles pairs, l'accord est dit « renversé » ; la note la plus grave est appelée « basse ».
De manière plus générale, l'accord est dit à l'état fondamental lorsque la basse est aussi la fondamentale. On a donc un état idéal de l'accord (état canonique) — un empilement strict de tierces — et l'état réel de l'accord — l'empilement des notes réellement jouées, avec d'éventuels redoublements, omissions et inversions ; et seule la basse indique si l'accord est à l'état fondamental ou renversé.
Le chiffrage dit de « basse continue » ''({{lang|it|basso continuo}})'' désigne la représentation d'un accord sous la forme d'un ou plusieurs chiffres arabes et éventuellement d'un chiffre romain.
==== Accords de trois notes ====
En musique classique, les seuls accords considérés comme parfaitement consonants, c'est-à-dire sonnant agréablement à l'oreille, sont appelés « accords parfaits ». Si l'on prend une tonalité et un mode donné, alors l'accord construit par superposition es degrés I, III et V de cette gamme porte le nom de la gamme qui l'a généré.
[[fichier:Accord do majeur chiffre.svg|vignette|upright=0.5|Accord parfait de ''do'' majeur chiffré.]]
Par exemple :
* « l'accord parfait de ''do'' majeur » est composé des notes ''do'', ''mi'' et ''sol'' ;
* « l'accord parfait de ''la'' mineur » est composé des notes ''la'', ''do'' et ''mi''.
Un accord parfait majeur est donc composé, en partant de la fondamentale, d'une tierce majeure et d'une quinte juste. Un accord parfait mineur est composé d'une tierce mineure et d'une quinte juste.
L'accord parfait à l'état fondamental est appelé « accord de quinte » et est simplement chiffré « 5 » pour indiquer la quinte.
On peut également commencer un accord sur sa deuxième ou sa troisième note, en faisant monter celle(s) qui précède(nt) à l'octave suivante. On parle alors de « renversement d'accord » ou d'accord « renversé ».
[[Fichier:Accord do majeur renversements chiffre.svg|vignette|upright=0.75|Accord parfait de ''do'' majeur et ses renversements, chiffrés.]]
Par exemple,
* le premier renversement de l'accord parfait de ''do'' majeur est :<br /> ''mi'', ''sol'', ''do'' ;
* le second renversement de l'accord parfait de do majeur est :<br /> ''sol'', ''do'', ''mi''.
Les notes conservent leur nom de « fondamentale », « tierce » et « quinte » malgré le changement d'ordre. La note la plus grave est appelée « basse ».
Dans le cas du premier renversement, le deuxième note est la tierce de la basse (la note la plus grave) et la troisième note est la sixte ; le chiffrage en chiffres arabes est donc « 6 » (puisque l'on omet la tierce) et l'accord est appelé « accord de sixte ». Pour le deuxième renversement, les intervalles sont la quarte et la sixte, le chiffrage est donc « 6-4 » et l'accord est appelé « accord de sixte et de quarte ».
Dans tous les cas, on chiffre le degré on considérant la fondamentale, par exemple {{Times New Roman|I}} si l'accord est construit sur la tonique de la gamme.
Les autres accords de trois notes que l'on rencontre sont :
* l'accord de quinte diminuée, constitué d'une tierce mineure et d'une quinte diminuée ; lorsqu'il est construit sur le septième degré d'une gamme, on considère que c'est un accord de septième de dominante sans fondamentale (voir plus bas), le degré est donc indiqué « “{{Times New Roman|V}}” » (cinq entre guillemets) et non « {{Times New Roman|VII}} » ;
* l'accord de quinte augmenté : il est composé d'une tierce majeure et qu'une quinte augmentée.
Dans le tableau ci-dessous,
* « m » désigne un intervalle mineur ;
* « M » un intervalle majeur ou le mode majeur ;
* « J » un intervalle juste ;
* « d » un intervalle diminué ;
* « A » un intervalle augmenté ;
* « mh » le mode mineur harmonique ;
* « ma » le mode mineur ascendant ;
* « md » le mode mineur descendant.
{| class="wikitable"
|+ Accords de trois notes
! scope="col" rowspan="2" | Nom
! scope="col" rowspan="2" | 3<sup>ce</sup>
! scope="col" rowspan="2" | 5<sup>te</sup>
! scope="col" rowspan="2" | État fondamental
! scope="col" rowspan="2" | 1<sup>er</sup> renversement
! scope="col" rowspan="2" | 2<sup>nd</sup> renversement
! scope="col" colspan="4"| Construit sur les degrés
|-
! scope="col" | M
! scope="col" | mh
! scope="col" | ma
! scope="col" | md
|-
| Accord parfait<br /> majeur || M || J
| accord de quinte || accord de sixte || accord de<br />sixte et de quarte
| {{Times New Roman|I, IV, V}} || {{Times New Roman|V, VI}} || {{Times New Roman|IV, V}} || {{Times New Roman|III, VI, VII}}
|-
| Accord parfait<br /> mineur || m || J
| accord de quinte || accord de sixte || accord de<br />sixte et de quarte
| {{Times New Roman|II, III, VI}} || {{Times New Roman|I, IV}} || {{Times New Roman|I, II}} || {{Times New Roman|I, IV, V}}
|-
| Accord de<br />quinte diminuée || m || d
| accord de<br />quinte diminuée || accord de<br />sixte sensible<br />sans fondamentale || accord de triton<br />sans fondamentale
| {{Times New Roman|VII (“V”)}} || {{Times New Roman|II, VII (“V”)}} || {{Times New Roman|VI, VII (“V”)}} || {{Times New Roman|II}}
|-
| Accord de<br />quinte augmentée || M || A
| accord de<br />quinte augmentée || accord de sixte<br />et de tierce sensible || accord de sixte et de quarte<br />sur sensible
| || {{Times New Roman|III}} || {{Times New Roman|III}} ||
|}
==== Accords de quatre notes ====
Les accords de quatre notes sont des accord composés de trois tierces superposées. La dernière note étant le septième degré de la gamme, on parle aussi d'accords de septième.
Ces accords sont dissonants : ils contiennent un intervalle de septième (soit une octave montante suivie d'une seconde descendante). Ils laissent donc une impression de « tension ».
Il existe sept différents types d'accords, ou « espèces ». Citons l'accord de septième de dominante, l'accord de septième mineure et l'accord de septième majeure.
===== L'accord de septième de dominante =====
[[Fichier:Accord 7e dominante do majeur renversements chiffre.svg|vignette|Accord de septième de dominante de ''do'' majeur et ses renversements, chiffrés.]]
L'accord de septième de dominante est l'empilement de trois tierces à partir de la dominante de la gamme, c'est-à-dire du {{Times New Roman|V}}<sup>e</sup> degré. Par exemple, l'accord de septième de dominante de ''do'' majeur est l'accord ''sol''-''si''-''ré''-''fa'', et l'accord de septième de dominante de ''la'' mineur est ''mi''-''sol''♯-''si''-''ré''. L'accord de septième de dominante dont la fondamentale est ''do'' (''do''-''mi''-''sol''-''si''♭) appartient à la gamme de ''fa'' majeur.
Que le mode soit majeur ou mineur, il est composé d'une tierce majeure, d'une quinte juste et d'une septième mineure (c'est un accord parfait majeur auquel on ajoute une septième mineure). C'est de loin l'accord de septième le plus utilisé ; il apparaît au {{pc|xvii}}<sup>e</sup> en musique classique.
Dans son état fondamental, son chiffrage est {{Times New Roman|V 7/+}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}). Le signe plus indique la sensible.
Son premier renversement est appelé « accord de quinte diminuée et sixte » et est noté {{Times New Roman|V 6/<s>5</s>}} (ou {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}).
Son deuxième renversement est appelé « accord de sixte sensible », puisque la sixte de l'accord est la sensible de la gamme, et est noté {{Times New Roman|V +6}} (ou {{Times New Roman|V<sup>+6</sup>}}).
Son troisième renversement est appelé « accord de quarte sensible » et est noté {{Times New Roman|V +4}} (ou {{Times New Roman|V<sup>+4</sup>}}).
[[Fichier:Accord 7e dominante sans fondamentale do majeur renversements chiffre.svg|vignette|Accord de septième de dominante sans fondamentale de ''do'' majeur et ses renversements, chiffrés.]]
On utilise aussi l'accord de septième de dominante sans fondamentale ; c'est alors un accord de trois notes.
Dans son état fondamental, c'est un « accord de quinte diminuée » placé sur le {{Times New Roman|VII}}<sup>e</sup> degré (mais c'est bien un accord construit sur le {{Times New Roman|V}}<sup>e</sup> degré), noté {{Times New Roman|“V” <s>5</s>}} (ou {{Times New Roman|“V”<sup><s>5</s></sup>}}). Notez les guillemets qui indiquent que la fondamentale V est absente.
Dans son premier renversement, c'est un « accord de sixte sensible sans fondamentale » noté {{Times New Roman|“V” +6/3}} (ou {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}}).
Dans son second renversement, c'est un « accord de triton sans fondamentale » (puisque le premier intervalle est une quarte augmentée qui comporte trois tons) noté {{Times New Roman|“V” 6/+4}} (ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}).
Notons qu'un accord de septième de dominante n'a pas toujours la dominante pour fondamentale : tout accord composé d'une tierce majeure, d'une quinte juste et d'une septième mineure est un accord de septième de dominante et est chiffré {{Times New Roman|<sup>7</sup><sub>+</sub>}}, quel que soit le degré sur lequel il est bâti (certaines notes peuvent avoir une altération accidentelle).
===== Les accords de septième d'espèce =====
Les autres accords de septièmes sont dits « d'espèce ».
L'accord de septième mineure est l'accord de septième formé sur la fondamentale d'une gamme mineure ''naturelle''. Par exemple, l'accord de septième mineure de ''la'' est ''la''-''do''-''mi''-''sol''. Il est composé d'une tierce mineure, d'une quinte juste et d'une septième mineure (c'est un accord parfait mineur auquel on ajoute une septième mineure).
L'accord de septième majeure est l'accord de septième formé sur la fondamentale d'une gamme majeure. Par exemple, L'accord de septième majeure de ''do'' est ''do''-''mi''-''sol''-''si''. Il est composé d'une tierce majeure, d'une quinte juste et d'une septième majeure (c'est un accord parfait majeur auquel on ajoute une septième majeure).
==== Utilisation du chiffrage ====
Le chiffrage est utilisé de deux manières.
La première manière, c'est la notation de la basse continue. La basse continue est une technique d'improvisation utilisée dans le baroque pour l'accompagnement d'instruments solistes. Sur la partition, on indique en général la note de basse de l'accord et le chiffrage en chiffres arabes.
La seconde manière, c'est pour l'analyse d'une partition. Le fait de chiffrer les accords permet de mieux en comprendre la structure.
De manière générale, on peut retenir que :
* le chiffrage « 5 » indique un accord parfait, superposition d'une tierce (majeure ou mineure) et d'une quinte juste ;
* le chiffrage « 6 » indique le premier renversement d'un accord parfait ;
* le chiffrage « 6/4 » indique le second renversement d'un accord parfait ;
* chiffrage « 7/+ » indique un accord de septième de dominante ;
* le signe « + » indique en général que la note de l'intervalle est la sensible ;
* un intervalle barré désigne un intervalle diminué.
[[fichier:Accords gamme do majeur la mineur.svg|class=transparent| center | Principaux accords construits sur les gammes de ''do'' majeur et de ''la'' mineur harmonique.]]
=== Notation « jazz » ===
En jazz et de manière générale en musique rock et populaire, la base d'un accord est la triade composée d'une tierce (majeure ou mineure) et d'une quinte juste. Pour désigner un accord, on utilise la note fondamentale, éventuellement désigné par une lettre dans le système anglo-saxon (A pour ''la'' etc.), suivi d'une qualité (comme « m », « + »…).
Les renversements ne sont pas notés de manière particulière, ils sont notés comme les formes fondamentales.
Dans les deux tableaux suivants, la fondamentale est notée X (remplace le C pour un accord de ''do'', le D pour un accord de ''ré''…). La construction des accords est décrite par la suite.
[[Fichier:Arbre accords triades 5d5J5A.svg|vignette|upright=1.5|Formation des triades présentée sous forme d'arbre.]]
{| class="wikitable"
|+ Notation des principales triades
|-
|
! scope="col" | Tierce<br />mineure (3m)
! scope="col" | Tierce<br />majeure (3M)
|-
! scope="row" | Quinte diminuée (5d)
| X<sup>o</sup>, Xm<sup>♭5</sup>, X–<sup>♭5</sup> ||
|-
! scope="row" | Quinte juste (5J)
| Xm, X– || X
|-
! scope="row" | Quinte augmentée (5A)
| || X+, X<sup>♯5</sup>
|}
[[Fichier:Triades do.svg|class=transparent|center|Triades de do.]]
{| class="wikitable"
|+ Notation des principaux accords de septième
|-
| colspan="2" |
! scope="col" | Tierce<br />mineure (3m)
! scope="col" | Tierce<br />majeure (3M)
|-
! scope="row" rowspan="2" | Quinte<br />diminuée (5d)
! scope="row" | Septième diminuée (7d)
| X<sup>o7</sup> ||
|-
! scope="row" | Septième mineure (7m)
| Xm<sup>7(♭5)</sup>, X–<sup>7(♭5)</sup>, X<sup>Ø</sup> ||
|-
! scope="row" rowspan="3" | Quinte<br />juste (5J)
! scope="row" | Sixte majeure (6M)
| Xm<sup>6</sup> || X<sup>6</sup>
|-
! scope="row" | Septième mineure (7m)
| Xm<sup>7</sup>, X–<sup>7</sup> || X<sup>7</sup>
|-
! scope="row" | Septième majeure (7M)
| Xm<sup>maj7</sup>, X–<sup>maj7</sup>, Xm<sup>Δ</sup>, X–<sup>Δ</sup> || X<sup>maj7</sup>, X<sup>Δ</sup>
|-
! scope="row" rowspan="2" | Quinte<br />augmentée (5A)
! scope="row" | Septième mineure (7m)
| || X+<sup>7</sup>
|-
! scope="row" | Septième majeure (7M)
| || X+<sup>maj7</sup>
|}
[[Fichier:Arbre accords septieme.svg|class=transparent|center|Formation des accords de septième présentée sous forme d'arbre.]]
[[Fichier:Accords do septieme.svg|class=transparent|center|Accord de do septième.]]
On notera que l'intervalle de sixte majeure est l'enharmonique de celui de septième diminuée (6M = 7d).
[[File:Principaux accords do.svg|class=transparent|center|Principaux accords de do.]]
==== Triades ====
; Accords fondés sur une tierce majeure
* accord parfait majeur : pas de notation
*: p. ex. « ''do'' » ou « C » pour l'accord parfait de ''do'' majeur (''do'' - ''mi'' - ''sol'')
; Accords fondés sur une tierce mineure
* accord parfait mineur : « m », « min » ou « – »
*: « ''do'' m », « ''do'' – », « Cm », « C– »… pour l'accord parfait de ''do'' mineur (''do'' - ''mi''♭ - ''sol'')
==== Triades modifiées ====
; Accords fondés sur une tierce majeure
* accord augmenté (la quinte est augmentée) : aug, +, ♯5
*: « ''do'' aug », « ''do'' + », « ''do''<sup>♯5</sup> » « Caug », « C+ » ou « C<sup>♯5</sup> » pour l'accord de ''do'' augmenté (''do'' - ''mi'' - ''sol''♯)
: L'accord augmenté est un empilement de tierces majeures. Ainsi, un accord augmenté a deux notes communes avec deux autres accords augmentés : C+ (''do'' - ''mi'' - ''sol''♯) a deux notes communes avec A♭+ (''la''♭ - ''do'' - ''mi'') et avec E+ (''mi'' - ''sol''♯ - ''si''♯) ; et on remarque que ces trois accords sont en fait enharmoniques (avec les enharmonies ''la''♭ = ''sol''♯ et ''si''♯ = ''do''). En effet, l'octave comporte six tons (sous la forme de cinq tons et deux demi-tons), et une tierce majeure comporte deux tons, on arrive donc à l'octave en ajoutant une tierce majeure à la dernière note de l'accord.
; Accords fondés sur une tierce mineure
* accord diminué (la quinte est diminuée) : dim, o, ♭5
*: « ''do'' dim », « ''do''<sup>o</sup> », « ''do''<sup>♭5</sup> », « Cdim », « C<sup>o</sup> » ou « C<sup>♭5</sup> » pour l'accord de ''do'' diminuné (''do'' - ''mi''♭ - ''sol''♭)
: On remarque que la quinte diminuée est l'enharmonique de la quarte augmentée et est l'intervalle appelé « triton » (car composé de trois tons).
; Accords fondés sur une tierce majeure ou mineure
* accord suspendu de seconde : la tierce est remplacée par une seconde majeure : sus2
*: « ''do''<sup>sus2</sup> » ou « C<sup>sus2</sup> » pour l'accord de ''do'' majeur suspendu de seconde (''do''-''ré''-''sol'')
* accord suspendu de quarte : la tierce est remplacée par une quarte juste : sus4
*: « ''do''<sup>sus4</sup> » ou « C<sup>sus4</sup> » pour l'accord de ''do'' majeur suspendu de quarte (''do''-''fa''-''sol'')
==== Triades appauvries ====
; Accords fondés sur une tierce majeure ou mineure
* accord de puissance : la tierce est omise, l'accord n'est constitué que de la fondamentale et de la quinte juste : 5
*: « ''do''<sup>5</sup> », « C<sup>5</sup> » pour l'accord de puissance de ''do'' (''do'' - ''la'')
{{note|Très utilisé dans les musiques rock, hard rock et heavy metal, il est souvent joué renversé (''la'' - ''do'') ou bien avec l'ajout de l'octave (''do'' - ''la'' - ''do'').}}
==== Triades enrichies ====
; Accords fondés sur une tierce majeure
* accord de septième (la 7<sup>e</sup> est mineure) : 7
*: « ''do''<sup>7</sup> », « C<sup>7</sup> » pour l'accord de ''do'' septième, appelé « accord de septième de dominante de ''fa'' majeur » en musique classique (''do'' - ''mi'' - ''sol'' - ''si''♭)
* accord de septième majeure : Δ, 7M ou maj7
*: « ''do'' <sup>Δ</sup> », « ''do'' <sup>maj7</sup> », « C<sup>Δ</sup> », « C<sup>7M</sup> »… pour l'accord de ''do'' septième majeure (''do'' - ''mi'' - ''sol'' - ''si'')
; Accords fondés sur une tierce mineure
* accord de mineur septième (la tierce et la 7<sup>e</sup> sont mineures) : m7, min7 ou –7
*: « ''do'' m<sup>7</sup> », « ''do'' –<sup>7</sup> », « Cm<sup>7</sup> », « C–<sup>7</sup> »… pour l'accord de ''do'' mineur septième, appelé « accord de septième de dominante de ''fa'' mineur » en musique classique (''do'' - ''mi''♭ - ''sol'' - ''si''♭)
* accord mineure septième majeure : m7M, m7maj, mΔ, –7M, –7maj, –Δ
*: « ''do'' m<sup>7M</sup> », « ''do'' m<sup>maj7</sup> », « ''do'' –<sup>Δ</sup> », « Cm<sup>7M</sup> », « Cm<sup>maj7</sup> », « C–<sup>Δ</sup> »… pour l'accord de ''do'' mineur septième majeure (''do'' - ''mi''♭ - ''sol'' - ''si'')
* accord de septième diminué (la quinte et la septième sont diminuée) : dim 7 ou o7
*: « ''do'' dim<sup>7</sup> », « ''do''<sup>o7</sup> », « Cdim<sup>7</sup> » ou « C<sup>o7</sup> » pour l'accord de ''do'' septième diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si''♭)
* accord demi-diminué (seule la quinte est diminuée, la septième est mineure) : Ø ou –7(♭5)
*: « ''do''<sup>Ø</sup> », « ''do''<sup>7(♭5)</sup> », « C<sup>Ø</sup> » ou « C<sup>7♭5</sup> » pour l'accord de ''do'' demi-diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si'')
=== Construction pythagoricienne des accords ===
Nous avons vu au débuts que lorsque l'on joue deux notes en même temps, leurs vibrations se superposent. Certaines superpositions créent un phénomène de battement désagréable, c'est le cas des secondes.
Dans le cas d'une tierce majeure, les fréquences des notes quadruple et quintuple d'une même base : les fréquences s'écrivent 4׃<sub>0</sub> et 5׃<sub>0</sub>. Cette superposition de vibrations est agréable à l'oreille. Nous avons également vu que dans le cas d'une quinte juste, les fréquences sont le double et le triple d'une même base, ou encore le quadruple et sextuple si l'on considère la moitié de cette base.
Ainsi, dans un accord parfait majeur, les fréquences des fondamentales des notes sont dans un rapport 4, 5, 6. De même, dans le cas d'un accord parfait mineur, les proportions sont de 1/6, 1/5 et 1/4.
{{voir|[[../Caractéristiques_et_notation_des_sons_musicaux#Construction_pythagoricienne_et_gamme_de_sept_tons|Caractéristiques et notation des sons musicaux > Construction pythagoricienne et gamme de sept tons]]}}
=== Un peu de physique : interférences ===
Les sons sont des vibrations. Lorsque l'on émet deux sons ou plus simultanément, les vibrations se superposent, on parle en physique « d'interférences ».
Le modèle le plus simple pour décrire une vibration est la [[w:fr:Fonction sinus|fonction sinus]] : la pression de l'air P varie en fonction du temps ''t'' (en secondes, s), et l'on a pour un son « pur » :
: P(''t'') ≈ sin(2π⋅ƒ⋅''t'')
où ƒ est la fréquence (en hertz, Hz) du son.
Si l'on émet deux sons de fréquence respective ƒ<sub>1</sub> et ƒ<sub>2</sub>, alors la pression vaut :
: P(''t'') ≈ sin(2π⋅ƒ<sub>1</sub>⋅''t'') + sin(2π⋅ƒ<sub>2</sub>⋅''t'').
Nous avons ici une [[w:fr:Identité trigonométrique#Transformation_de_sommes_en_produits,_ou_antilinéarisation|identité trigonométrique]] dite « antilinéarisation » :
: <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{f_1 + f_2}{2}t \right ) \cdot \sin \left ( 2\pi \frac{f_1 - f_2}{2}t \right ).</math>
On peut étudier simplement deux situations simples.
[[Fichier:Battements interferentiels.png|vignette|Deux sons de fréquences proches créent des battements : la superposition d'une fréquence et d'une enveloppe.]]
La première, c'est quand les fréquences ƒ<sub>1</sub> et ƒ<sub>2</sub> sont très proches. Alors, la moyenne (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2 est très proche de ƒ<sub>1</sub> et ƒ<sub>2</sub> ; et la demie différence (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2 est très proche de zéro. On a donc une enveloppe de fréquence très faible, (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2, dans laquelle s'inscrit un son de fréquence moyenne, (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2. C'est cette enveloppe de fréquence très faible qui crée les battements, désagréables à l'oreille.
Sur l'image ci-contre, le premier trait rouge montre un instant où les vibrations sont opposées ; elles s'annulent, le son s'éteint. Le second trait rouge montre un instant où les vibrations sont en phase : elle s'ajoutent, le son est au plus fort.
{{clear}}
La seconde, c'est lorsque les deux fréquences sont des multiples entiers d'une même fréquence fondamentale ƒ<sub>0</sub> : ƒ<sub>1</sub> = ''n''<sub>1</sub>⋅ƒ<sub>0</sub> et ƒ<sub>0</sub> = ''n''<sub>0</sub>⋅ƒ<sub>0</sub>. On a alors :
: <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{n_1 + n_2}{2}f_0 \cdot t \right ) \cdot \sin \left ( 2\pi \frac{n_1 - n_2}{2}f_0 \cdot t \right ).</math>
On multiplie donc deux fonctions qui ont des fréquences multiples de ƒ<sub>0</sub>. La différence minimale entre ''n''<sub>1</sub> et ''n''<sub>2</sub> vaut 1 ; on a donc une enveloppe dont la fréquence est au minimum la moitié de ƒ<sub>0</sub>, c'est-à-dire un son une octave en dessous de ƒ<sub>0</sub>. Donc, cette enveloppe ne crée pas d'effet de battement, ou plutôt, le battement est trop rapide pour être perçu comme tel. Dans cette enveloppe, on a une fonction sinus dont la fréquence est également un multiple de ƒ<sub>0</sub> ; l'enveloppe et la fonction qui y est inscrite ont donc de nombreux « points communs », d'où l'effet harmonieux.
=== Le tonnetz ===
[[File:Speculum musicae.png|thumb|right|225px|Euler, ''De harmoniæ veris principiis'', 1774, p. 350.]]
En allemand, le terme ''Tonnetz'' (se prononce « tône-netz ») signifie « réseau tonal ». C'est une représentation graphique des notes qui a été imaginée par [[w:Leonhard Euler|Leonhard Euler]] en 1739.
Cette représentation graphique peut aider à la mémorisation de certains concepts de l'harmonie. Cependant, son application est très limitée : elle ne concerne que l'intonation juste d'une part, et que les accords parfait des tonalités majeures et mineures naturelles d'autre part. La représentation contenant les douze notes de la musique savante occidentale, on peut bien sûr représenter d'autres objets, comme les accords de septième ou les accords diminués, mais la représentation graphique est alors compliquée et perd son intérêt pédagogique.
On part d'une note, par exemple le ''do''. Si on progresse vers la droite, on monte d'une quinte juste, donc ''sol'' ; vers la gauche, on descend d'une quinte juste, donc ''fa''. Si on va vers le bas, on monte d'une tierce majeure, donc ''mi'' ; si on va vers le haut, on descend d'une tierce majeure, donc ''la''♭ ou ''sol''♯
fa — do — sol — ré
| | | |
la — mi — si — fa♯
| | | |
do♯ — sol♯ — ré♯ — si♭
La figure forme donc un filet, un réseau. On voit que ce réseau « boucle » : si on descend depuis le ''do''♯, on monte d'une tierce majeure, on obtient un ''mi''♯ qui est l'enharmonique du ''fa'' qui est en haut de la colonne. Si on va vers la droite à partir du ''ré'', on obtient le ''la'' qui est au début de la ligne suivante.
Si on ajoute des diagonales allant vers la droite et le haut « / », on met en évidence des tierces mineures : ''la'' - ''do'', ''mi'' - ''sol'', ''si'' - ''ré'', ''do''♯ - ''mi''…
fa — do — sol — ré
| / | / | / |
la — mi — si — fa♯
| / | / | / |
do♯ — sol♯ — ré♯ — si♭
Donc les liens représentent :
* | : tierce majeure ;
* — : quinte juste ;
* / : tierce mineure.
[[Fichier:Tonnetz carre accords fr.svg|thumb|Tonnetz avec les accords parfaits. Les notes sont en notation italienne et les accords en notation jazz.]]
On met ainsi en évidence des triangles dont un côté est une quinte juste, un côté une tierce majeure et un côté une tierce mineure ; c'est-à-dire que les notes aux sommets du triangle forment un accord parfait majeur (par exemple ''do'' - ''mi'' - ''sol'') :
<div style="font-family:courier; background-color:#fafafa">
fa — '''do — sol''' — ré<br />
| / '''| /''' | / |<br />
la — '''mi''' — si — fa♯<br />
| / | / | / |<br />
do♯ — sol♯ — ré♯ — si♭
</div>
ou un accord parfait mineur (''la'' - ''do'' - ''mi'').
<div style="font-family:courier; background-color:#fafafa">
fa — '''do''' — sol — ré<br />
| '''/ |''' / | / |<br />
'''la — mi''' — si — fa♯<br />
| / | / | / |<br />
do♯ — sol♯ — ré♯ — si♭
</div>
Un triangle représente donc un accord, et un sommet représente une note. Si on passe d'un triangle à un triangle voisin, alors on passe d'un accord à un autre accord, les deux accords ayant deux notes en commun. Ceci illustre la notion de « plus court chemin » en harmonie : si on passe d'un accord à un autre en gardant un côté commun, alors on a un mouvement conjoint sur une seule des trois voix.
Par rapport à l'harmonie fonctionnelle : les accords sont contigus à leur fonction, par exemple en ''do'' majeur :
* fonction de tonique ({{Times New Roman|I}}) : C, A– et E– sont contigus ;
* fonction de sous-dominante ({{Times New Roman|IV}}) : F et D– sont contigus ;
* fonction de dominante ({{Times New Roman|V}}) : G et B<sup>o</sup> sont contigus.
On notera que les triangles d'un schéma ''tonnetz'' ne représentent que des accords parfaits. Pour représenter un accord de quinte diminuée (''si'' - ''ré'' - ''fa'') ou les accords de septième, en particulier l'accord de septième de dominante, il faut étendre le ''tonnetz'' et l'on obtient des figures différentes. Par ailleurs, il est adapté à ce que l'on appelle « l'intonation juste », puisque tous les intervalles sont idéaux.
[[Fichier:Tonnetz carre accords etendu fr.svg|vignette|Tonnetz étendu.]]
[[Fichier:Tonnetz carre do majeur accords fr.svg|vignette|Tonnetz de la tonalité de ''do'' majeur. La représentation de l'accord de quinte diminuée sur ''si'' (B<sup>o</sup>) est une ligne et non un triangle.]]
[[Fichier:Tonnetz carre do mineur accords fr.svg|vignette|Tonnetz des tonalités de ''do'' mineur naturel (haut) et ''do'' mineur harmonique (bas).]]
Si l'on étend un peu le réseau :
ré♭ — la♭ — mi♭ — si♭ — fa
| / | / | / | / |
fa — do — sol — ré — la
| / | / | / | / |
la — mi — si — fa♯ — do♯
| / | / | / | / |
do♯ — sol♯ — ré♯ — la♯ — mi♯
| / | / | / | / |
mi♯ — do — sol — ré — la
on peut donc trouver des chemins permettant de représenter les accords de septième de dominante (par exemple en ''do'' majeur, G<sup>7</sup>)
fa
/
sol — ré
| /
si
et des accords de quinte diminuée (en ''do'' majeur : B<sup>o</sup>)
fa
/
ré
/
si
Une gamme majeure ou mineure naturelle peut se représenter par un trapèze rectangle : ''do'' majeur
fa — do — sol — ré
| /
la — mi — si
et ''do'' mineur
la♭ — mi♭ — si♭
/ |
fa — do — sol — ré
En revanche, la représentation d'une tonalité nécessite d'étendre le réseau afin de pouvoir faire figurer tous les accords, deux notes sont représentées deux fois. La représentation des tonalités mineures harmoniques prend une forme biscornue, ce qui nuit à l'intérêt pédagogique de la représentation.
[[Fichier:Neo-Riemannian Tonnetz.svg|vignette|upright=2|Tonnetz avec des triangles équilatéraux.]]
On peut réorganiser le schéma en décalant les lignes, afin d'avoir des triangles équilatéraux. Sur la figure ci-contre (en notation anglo-saxonne) :
* si on monte en allant vers la droite « / », on a une tierce mineure ;
* si on descend en allant vers la droite « \ », on a une tierce majeure ;
* les liens horizontaux « — » représentent toujours des quintes justes
* les triangles pointe en haut sont des accords parfaits mineurs ;
* les triangles pointe en bas sont des accords parfaits majeurs.
On a alors les accords de septième de dominante
F
/
G — D
\ /
B
et de quinte diminuée
F
/
D
/
B
les tonalités majeures
F — C — G — D
\ /
A — E — B
et les tonalités mineures naturelles
A♭ — E♭ — B♭
/ \
F — C — G — D
== Notes et références ==
{{références}}
== Voir aussi ==
=== Liens externes ===
{{wikipédia|Consonance (harmonie tonale)}}
{{wikipédia|Disposition de l'accord}}
{{wikisource|Petit Manuel d’harmonie}}
* {{lien web
| url = https://www.apprendrelesolfege.com/chiffrage-d-accords
| titre = Chiffrage d'accords (classique)
| site = Apprendrelesolfege.com
| consulté le = 2020-12-03
}}
* {{lien web
| url = https://www.coursd-harmonie.fr/introduction/introduction2_le_chiffrage_d_accords.php
| titre = Introduction II : Le chiffrage d'accords
| site = Cours d'harmonie.fr
| consulté le = 2021-12-14
}}
* {{lien web
| url=https://www.coursd-harmonie.fr/
| titre = Cours d'harmonie en ligne
| auteur = Jean-Baptiste Voinet
| site=coursd-harmonie.fr
| consulté le = 2021-12-20
}}
* {{lien web
| url=http://e-harmonie.e-monsite.com/
| titre = Cours d'harmonie classique en ligne
| auteur = Olivier Miquel
| site=e-harmonie
| consulté le = 2021-12-24
}}
* {{lien web
| url=https://fr.audiofanzine.com/theorie-musicale/editorial/dossiers/les-gammes-et-les-modes.html
| titre = Les bases de l’harmonie
| site = AudioFanzine
| date = 2013-07-23
| consulté le = 2024-01-12
}}
----
''[[../Mélodie|Mélodie]]'' < [[../|↑]] > ''[[../Représentation musicale|Représentation musicale]]''
[[Catégorie:Formation musicale (livre)|Harmonie]]
iwk8lcjabfvsoasiqdyusf7e5i8ku62
745867
745866
2025-07-03T11:45:48Z
Cdang
1202
/* Harmonisation par des accords de septième */
745867
wikitext
text/x-wiki
{{Bases de solfège}}
<span style="font-size:25px;">6. Harmonie</span>
L'harmonie désigne les notes jouées en même temps, soit plusieurs instruments jouant chacun une note, soit un instrument jouant un accord (instrument dit polyphonique).
== Première approche ==
L'exemple le plus simple d'harmonie est sans doute la chanson en canon : c'est un chant polyphonique, c'est-à-dire à plusieurs voix, chaque voix chantant la même chose en décalé. Prenons par exemple ''Vent frais, vent du matin'' (la version originale est ''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609) :
[[Fichier:Vent frais vent du matin.svg|class=transparent|center|Partition de ''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]]
[[Fichier:Vent frais vent du matin.midi|vignette|''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]]
nous voyons que les voix se superposent de manière « harmonieuse ». Les notes de chaque voix se correspondent point par point (avec un retard), c'est donc un type d'harmonie polyphonique appelé « contrepoint ».
Considérons la première note de la mesure 6 pour chaque voix. Nous avons la superposition des notes ''ré''-''fa''-''la'' (du grave vers l'aigu) ; la superposition de notes jouées ou chantées ensembles s'appelle un accord. Cet accord ''ré''-''fa''-''la'' porte le nom « d'accord parfait de ''ré'' mineur » :
* « ''ré'' » car la note fondamentale est un ''ré'' ;
* « parfait » car il est l'association d'une tierce, ''ré''-''fa'', et d'une quinte juste, ''ré''-''la'' ;
* « mineur » car le premier intervalle, ''ré''-''fa'', est une tierce mineure.
Considérons maintenant un chant accompagné au piano. La piano peut jouer plusieurs notes en même temps, il peut jouer des accords.
[[Fichier:Au clair de le lune chant et piano.svg|class=transparent|center|Deux premières mesure d’Au clair de la lune.]]
[[Fichier:Au clair de le lune chant et piano.midi|vignette|Deux premières mesure d’Au clair de la lune.]]
L'accord, les notes à jouer simultanément, sont écrites « en colonne ». Lorsqu'on les énonce, on les lit de bas en haut mais le pianiste les joue en pressant les touches du clavier en même temps, de manière « plaquée ».
Le premier accord est composé des notes ''do''-''mi''-''sol'' ; il est appelé « accord parfait de ''do'' majeur » car la note fondamentale est ''do'', qu'il est l'association d'une tierce et d'une quinte juste et que le premier intervalle, ''do''-''mi'', est une tierce majeure.
== Consonance et dissonance ==
Les notions de consonance et de dissonance sont culturelles et changent selon l'époque. Nous pouvons néanmoins noter que :
* l'accord de seconde, et son renversement la septième, créent des battements, les notes « frottent », c'est un intervalle harmonique dissonant ; mais dans le cas de la septième, comme les notes sont éloignées, le frottement est moins perceptible ;
* les accords de tierce, quarte et quinte sonnent agréablement à l'oreille, ils sont consonants.
Dans la musique savante européenne, au début au du Moyen-Âge, seuls les accords de quarte et de quinte étaient considérés comme consonants, d'où leur qualification de « juste ». La tierce, et son renversement la sixte, étaient perçues comme dissonantes.
L'harmonie joue avec les consonances et les dissonances. Dans un premier temps, les harmonies dissonantes sont utilisées pour créer des tensions qui sont ensuite résolues, on utilise des successions « consonant-dissonant-consonant ». À force d'entendre des intervalles considérés comme dissonants, l'oreille s'habitue et certains finissent par être considérés comme consonants ; c'est ce qui est arrivé à la tierce et à la sixte à la fin du Moyen Âge avec le contrepoint.
Il faut ici aborder la notion d'harmonique des notes.
[[File:Harmoniques de do.svg|thumb|Les six premières harmoniques de ''do''.]]
Lorsque l'on joue une note, on entend d'autres notes plus aigües et plus faibles ; la note jouée est appelée la « fondamentale » et les notes plus aigües et plus faibles sont les « harmoniques ». C'est cette accumulation d'harmoniques qui donne la couleur au son, son timbre, qui fait qu'un piano ne sonne pas comme un violon. Par exemple, si l'on joue un ''do''<sup>1</sup><ref>Pour la notation des octaves, voir ''[[../Représentation_musicale#Désignation_des_octaves|Représentation musicale > Désignation des octaves]]''.</ref> (fondamentale), on entend le ''do''<sup>2</sup> (une octave plus aigu), puis un ''sol''<sup>2</sup>, puis encore un ''do''<sup>3</sup> plus aigu, puis un ''mi''<sup>3</sup>, puis encore un ''sol''<sup>3</sup>, puis un ''si''♭<sup>3</sup>…
Ainsi, puisque lorsque l'on joue un ''do'' on entend aussi un ''sol'' très léger, alors jouer un ''do'' et un ''sol'' simultanément n'est pas choquant. De même pour ''do'' et ''mi''. De là vient la notion de consonance.
Le statut du ''si''♭ est plus ambigu. Il fait partie des harmoniques qui sonnent naturellement, mais il forme une seconde descendante avec le ''do'', intervalle dissonant. Par ailleurs, on remarque que le ''si''♭ ne fait pas partie de la gamme de ''do'' majeur, contrairement au ''sol'' et au ''mi''.
Pour le jeu sur les dissonances, on peut écouter par exemple la ''Toccata'' en ''ré'' mineur, op. 11 de Sergueï Prokofiev (1912).
: {{lien web |url=https://www.youtube.com/watch?v=AVpnr8dI_50 |titre=Yuja Wang Prokofiev Toccata |site=YouTube |date=2019-02-26 |consulté le=2021-12-19}}
== Contrepoint ==
Dans le chant grégorien, la notion d'accord n'existe pas. L'harmonie provient de la superposition de plusieurs mélodies, notamment dans ce que l'on appelle le « contrepoint ».
Le terme provient du latin ''« punctum contra punctum »'', littéralement « point par point », et désigne le fait que les notes de chaque voix se correspondent.
L'exemple le plus connu de contrepoint est le canon, comme par exemple ''Frère Jacques'' : chaque note d'un couplet correspond à une note du couplet précédent.
Certains morceaux sont bâtis sur une écriture « en miroir » : l'ordre des notes est inversé entre les deux voix, ou bien les intervalles sont inversés (« mouvement contraire » : une tierce montante sur une voix correspond à une tierce descendante sur l'autre).
On peut également citer le « mouvement oblique » (une des voix, le bourdon, chante toujours la même note) et le mouvement parallèle (les deux voix chantent le même air mais transposé, l'une est plus aiguë que l'autre).
Nous reproduisons ci-dessous le début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.
[[Fichier:Haendel Sonate en trio re mineur debut canon.svg | vignette | center | upright=2 | Début du second ''Allergo'' de la sonate en trio en ''ré'' mineur de Haendel.]]
[[Fichier:Haendel Sonate en trio re mineur debut.midi | vignette | Début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.]]
Nous avons mis en évidence la construction en canon avec des encadrés de couleur : sur les quatre premières mesures, nous voyons trois thèmes repris alternativement par une voix et par l'autre. Ce type de procédé est très courant dans la musique baroque.
Les procédés du contrepoint s'appliquent également à la danse :
* unisson : les danseurs et danseuses font les mêmes gestes en même temps ;
* répétition : le fait de répéter une série de gestes, une « phrase dansante » ;
* canon : les gestes sont faits avec un décalage régulier d'un danseur ou d'une danseuse à l'autre ;
* cascade : forme de canon dans laquelle le décalage est très petit ;
* contraste : deux danseur·euses, ou deux groupes, ont des gestuelles très différentes ;
* accumulation : la gestuelle se complexifie par l'ajout d'éléments au fur et à mesure ; ou bien le nombre de danseur·euses augmente ;
* dialogue : les gestes de danseur·euses ou de groupes se répondent ;
* contre-point : la gestuelle d'un ou une danseuse se superpose à la gestuelle d'un groupe ;
* lâcher-rattraper : les danseurs et danseuses alternent danse à l'unisson et gestuelles indépendantes.
: {{lien web
| url=https://www.youtube.com/watch?v=wgblAOzedFc
| titre=Les procédés de composition en danse
| auteur= Doisneau Sport TV
| site=YouTube
| date=2020-03-16 | consulté le=2021-01-21
}}
{{...}}
== Les accords en général ==
Initialement, on a des chants polyphoniques, des voix qui chantent chacune une mélodie, les mélodies se mêlant. On remarque que certaines superpositions de notes sonnent de manière plus ou moins agréables, consonantes ou dissonantes. On en vient alors à associer ces notes, c'est-à-dire à considérer dès le départ la superposition de ces notes et non pas la rencontre de ces notes au gré des mélodies. Ces groupes de notes superposées forment les accords. En Europe, cette notion apparaît vers le {{pc|xiv}}<sup>e</sup> siècle avec notamment la ''[[wikipedia:fr:Messe de Notre Dame|Messe de Notre Dame]]'' de Guillaume de Machaut (vers 1360-1365). La notion « d'accord parfait » est consacrée par [[wikipedia:fr:Jean-Philippe Rameau|Jean-Philippe Rameau]] dans son ''Traité de l'harmonie réduite à ses principes naturels'', publié en 1722.
=== Qu'est-ce qu'un accord ? ===
Un accord est un ensemble d'au minimum trois notes jouées en même temps. « Jouées » signifie qu'il faut qu'à un moment donné, elles sonnent en même temps, mais le début ou la fin des notes peut être à des instants différents.
Considérons que l'on joue les notes ''do'', ''mi'' et ''sol'' en même temps. Cet accord s'appelle « accord de ''do'' majeur ». En musique classique, on lui adjoint l'adjectif « parfait » : « accord parfait de ''do'' majeur ».
Nous représentons ci-dessous trois manière de faire l'accord : avec trois instruments jouant chacun une note :
[[Fichier:Do majeur trois portees.svg|class=transparent|center|Accord de ''do'' majeur avec trois instruments différents.]]
Avec un seul instrument jouant simultanément les trois notes :
[[Fichier:Chord C.svg|class=transparent|center|Accord de ''do'' majeur joué par un seul instrument.]]
L'accord tel qu'il est joué habituellement par une guitare d'accompagnement :
[[Fichier:Do majeur guitare.svg|class=transparent|center|Accord de ''do'' majeur à la guitare.]]
Pour ce dernier, nous représentons le diagramme indiquant la position des doigts sur le manche au dessus de la portée et la tablature en dessous. Ici, c'est au total six notes qui sont jouées : ''mi'' grave, ''do'' médium, ''mi'' médium, ''sol'' médium, ''do'' aigu, ''mi'' aigu. Mais il s'agit bien des trois notes ''do'', ''mi'' et ''sol'' jouées à des octaves différentes. Nous remarquons également que la note de basse (la note la plus grave), ''mi'', est différente de la note fondamentale (celle qui donne le nom à l'accord), ''do'' ; l'accord est dit « renversé » (voir plus loin).
=== Comment joue-t-on un accord ? ===
Les notes ne sont pas forcément jouées en même temps ; elles peuvent être « égrainées », jouée successivement, ce que l'on appelle un arpège. La partition ci-dessous montre six manières différentes de jouer un accord de ''la'' mineur à la guitare, plaqué puis arpégé.
[[Fichier:La mineur differentes executions.svg|class=transparent|center|Différentes exécution de l'accord de do majeur à la guitare.]]
[[Fichier:La mineur differentes executions midi.midi|vignette|Différentes exécution de l'accord de la mineur à la guitare.]]
Vous pouvez écouter l'exécution de cette partition avec le lecteur ci-contre.
Seuls les instruments polyphoniques peuvent jouer les accords plaqués : instruments à clavier (clavecin, orgue, piano, accordéon), les instruments à plusieurs cordes pincées (harpe, guitare ; violon, alto, violoncelle et contrebasse joués en pizzicati). Les instruments à corde frottés de la famille du violon peuvent jouer des notes par deux à l'archet mais pas plus du fait de la forme bombée du chevalet ; cependant, un mouvement rapide permet de jouer les quatre cordes de manière très rapprochée. Les instruments à percussion de type xylophone ou le tympanon permettent de jouer jusqu'à quatre notes simultanément en tenant deux baguettes (mailloches, maillets) par main.
Tous les instruments peuvent jouer des arpèges même si, dans le cas des instruments monodiques, les notes ne continuent pas à sonner lorsque l'on passe à la note suivante.
L'arpège peut être joué par l'instrument de basse (basson, violoncelle, contrebasse, guitare basse, pédalier de l'orgue…), notamment dans le cas d'une basse continue ou d'une ''{{lang|en|walking bass}}'' (« basse marchante » : la basse joue des noires, donnant ainsi l'impression qu'elle marche).
En jazz, et spécifiquement au piano, on a recours au ''{{lang|en|voicing}}'' : on choisit la manière dont on organise les notes pour donner une couleur spécifique, ou bien pour créer une mélodie en enchaînant les accords. Il est fréquent de ne pas jouer toutes les notes : si on n'en garde que deux, ce sont la tierce et la septième, car ce sont celles qui caractérisent l'accord (selon que la tierce est mineure ou majeure, que la septième est majeure ou mineure), et la fondamentale est en général jouée par la contrebasse ou guitare basse.
{{clear}}
=== Classes d'accord ===
[[Fichier:Intervalles harmoniques accords classes.svg|vignette|upright=1.5|Intervalles harmoniques dans les accords classés de trois, quatre et cinq notes.]]
Un accord composé d'empilement de tierces est appelé « accord classé ». En musique tonale, c'est-à-dire la musique fondée sur les gammes majeures ou mineures (cas majoritaire en musique classique), on distingue trois classes d'accords :
* les accords de trois notes, ou triades, ou accords de quinte ;
* les accords de quatre notes, ou accords de septième ;
* les accords de cinq notes, ou accords de neuvième.
En empilant des tierces, si l'on part de la note fondamentale, on a donc de intervalles de tierce, quinte, septième et neuvième.
En musique tonale, les accords avec d'autres intervalles (hors renversement, voir ci-après), typiquement seconde, quarte ou sixte, sont considérés comme des transitions entre deux accords classés. Ils sont appelés, selon leur utilisation, « accords à retard » (en anglais : ''{{lang|en|suspended chord}}'', accord suspendu) ou « appoggiature » (note « appuyée », étrangère à l'harmonie). Voir aussi plus loin la notion de note étrangère.
=== Renversements d'accords ===
[[File:Accord do majeur renversements.svg|thumb|Accord parfait de do majeur et ses renversements.]]
[[Fichier:Progression dominante renverse parfait do majeur.svg|vignette|upright=0.6|Progression accord de dominante renversé → accord parfait en ''do'' majeur.]]
Un accord classé est donc un empilement de tierces. Si l'on change l'ordre des notes, on a toujours le même accord mais il est fait avec d'autres intervalles harmoniques. Par exemple, l'accord parfait de ''do'' majeur dans son état fondamental, c'est-à-dire non renversé, s'écrit ''do'' - ''mi'' - ''sol''. Sa note fondamentale, ''do'', est aussi se note de basse.
Si maintenant on prend le ''do'' de l'octave supérieure, l'accord devient ''mi - sol - do'' ; c'est l'empilement d'une tierce ''(mi - sol)'' et d'une quarte ''(sol - do)'', soit la superposition d'une tierce ''(mi - sol)'' et d'une sixième ''(mi - do)''. C'est le premier renversement de l'accord parfait de ''do'' majeur ; la fondamentale est toujours ''do'' mais la basse est ''mi''. Le second renversement est ''sol - do - mi''.
L'utilisation de renversement peut faciliter l'exécution de la progression d'accord. Par exemple, en tonalité ''do'' majeur, si l'on veut passer de l'accord de dominante ''sol - si - ré'' à l'accord parfait ''do - mi - sol'', alors on peut utiliser le second renversement de l'accord de dominante : ''ré - sol - si'' → ''do - mi - sol''. Ainsi, la basse descend juste d'un ton ''(ré → do)'' et sur un piano, la main reste globalement dans la même position.
Le renversement d'un accord permet également de respecter certaines règles de l'harmonie classique, notamment éviter que des voix se suivent strictement (« mouvement parallèle »), ce qui aurait un effet de platitude.
De manière générale, la notion de renversement permet deux choses :
* d'enrichir l'œuvre : pour créer une harmonie donnée (c'est-à-dire des sons sonnant bien ensemble), nous avons plus de souplesse, nous pouvons organiser ces notes comme nous le voulons selon les voix ;
* de simplifier l'analyse : quelle que soit la manière dont sont organisées les notes, cela nous ramène à un même accord.
{{citation bloc|Or il, y a plusieurs manières de jouer un accord, selon que l'on aborde par la première note qui le constitue, ''do mi sol'', la deuxième, ''mi sol do'', ou la troisième note, ''sol do mi''. Ce sont les renversements, [que Rameau] va classer en différentes combinaisons d'une seule matrice. Faisant cela, Rameau divise le nombre d'accords [de septième] par quatre. Il simplifie, il structure […].|{{ouvrage|prénom1=André |nom1=Manoukian |titre=Sur les routes de la musique |éditeur=Harper Collins |année=2021 |passage=54 |isbn=979-1-03391201-9}} }}
{{clear}}
[[File:Plusieurs realisation 1er renversement doM.svg|thumb|Plusieurs réalisation du premier renversement de l'accord de ''do'' majeur.]]
Notez que dans la notion de renversement, seule importe en fait la note de basse. Ainsi, les accords ''mi-sol-do'', ''mi-do-sol'', ''mi-do-mi-sol'', ''mi-sol-mi-do''… sont tous une déclinaison du premier renversement de ''do-mi-sol'' et ils seront abrégés de la même manière (''mi''<sup>6</sup> en musique classique ou C/E en musique populaire et jazz, voir plus bas).
{{clear}}
== Notation des accords de trois notes ==
Les accords de trois notes sont appelés « accords de quinte » en classique, et « triades » en jazz.
[[Fichier:Progression dominante renverse parfait do majeur chiffrage.svg|vignette|upright=0.7|Chiffrage du second renversement d'un accord de ''sol'' majeur et d'un accord de ''do'' majeur : notation en musique populaire et jazz (haut) et notation de basse chiffrée (bas).]]
Les accords sont construits de manière systématique. Nous pouvons donc les représenter de manière simplifiée. Cette notation simplifiée des accords est appelée « chiffrage ».
Reprenons la progression d'accords ci-dessus : « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » dans la tonalité de ''do'' majeur. On utilise en général trois notations différentes :
* en musique populaire, jazz, rock… un accord est désigné par sa note fondamentale ; ici donc, les accords sont notés « ''sol'' - ''do'' » ou, en notation anglo-saxonne, « G - C » ;<br /> comme le premier accord est renversé, on indique la note de basse après une barre, la progression d'accords est donc chiffrée '''« ''sol''/''ré'' - ''do'' »''' ou '''« G/D - C »''' ;<br /> il s'agit ici d'accords composés d'une tierce majeure et d'une quinte juste ; si les accords sont constitués d'intervalles différents, nous ajoutons un symbole après la note : « m » ou « – » si la tierce est mineure, « dim » ou « ° » si la quinte est diminuée ;
* en musique classique, on utilise la notation de « basse chiffrée » (utilisée notamment pour noter la basse continue en musique baroque) : on indique la note de basse sur la portée et on lui adjoint l'intervalle de la fondamentale à la note la plus haute (donc ici respectivement 6 et 5, puisque ''sol''-''si'' est une sixte et ''do''-''sol'' est une quinte), étant sous-entendu que l'on a des empilements de tierce en dessous ; mais dans le cas du premier accord, le premier intervalle n'est pas une tierce, mais une quarte ''(ré''-''sol)'', on note donc '''« ''ré'' <sup>6</sup><sub>4</sub> - ''do'' <sup>5</sup> »'''<ref>quand on ne dispose pas de la notation en supérieur (exposant) et inférieur (indice), on utilise parfois une notation sous forme de fraction : ''sol'' 6/4 et ''do'' 5/.</ref> ;
* lorsque l'on fait l'analyse d'un morceau, on s'attache à identifier la note fondamentale de l'accord (qui est différente de la basse dans le cas d'un renversement) ; on indique alors le degré de la fondamentale : '''« {{Times New Roman|V<sup>6</sup><sub>4</sub> - I<sup>5</sup>}} »'''.
La notation de basse chiffrée permet de construire l'accord à la volée :
* on joue la note indiquée (basse) ;
* s'il n'y a pas de 2 ni de 4, on lui ajoute la tierce ;
* on ajoute les intervalles indiqués par le chiffrage.
La notation de musique jazz oblige à connaître la composition des différents accords, mais une fois que ceux-ci sont acquis, il n'y a pas besoin de reconstruire l'accord.
La notation de basse chiffrée avec les chiffres romains n'est pas utilisée pour jouer, mais uniquement pour analyser ; Sur les partitions avec basse chiffrée, il y a simplement les chiffrages indiqués au-dessus de la partie de basse. Le chiffrage avec le degré en chiffres romains présente l'avantage d'être indépendant de la tonalité et donc de se concentrer sur la fonction de l'accord au sein de la tonalité. Par exemple, ci-dessous, nous pouvons parler de la progression d'accords « {{Times New Roman|V - I}} » de manière générale, cette notation étant valable quelle que soit la tonalité.
[[File:Progression dominante renverse parfait do majeur chiffrage basse continue.svg|thumb|Chiffrage en notation basse chiffrée de la progression d'accords « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » en do majeur.]]
{{note|En notation de base continue avec fondamentale en chiffres romains, la fondamentale est toujours indiquée ''sous'' la portée de la partie de basse. Les intervalles sont indiqués au-dessus de la portée de la partie de basse ; lorsque l'on fait une analyse, on peut ayssi les indiquer à côté du degré en chiffres romains, donc sous la portée de la basse.}}
{{note|En notation rock, le 5 en exposant indique un accord incomplet avec uniquement la fondamentale et la quinte, un accord sans tierce appelé « accord de puissance » ou ''{{lang|en|power chord}}''. Par exemple, C<sup>5</sup> est l'accord ''do-sol''.}}
{{clear}}
[[Fichier:Accords parfait do majeur basse chiffree fondamental et renverse.svg|vignette|upright=2.5|Chiffrage de l'accord parfait de ''do'' majeur en basse chiffrée, à l'état fondamental et ses renversements.]]
Concernant les accords parfaits en notation de basse chiffrée :
* un accord parfait à l'état fondamental est chiffré « <sup>5</sup> » ; on l'appelle « accord de quinte » ;
* le premier renversement est chiffré « <sup>6</sup> » (la tierce est implicite) ; on l'appelle « accord de sixte » ;
* le second renversement est noté « <sup>6</sup><sub>4</sub> » ; on l'appelle « accord de sixte et de quarte » (ou bien « de quarte et de sixte »).
Par exemple, pour l'accord parfait de ''do'' majeur :
* l'état fondamental ''do''-''mi''-''sol'' est noté ''do''<sup>5</sup> ;
* le premier renversement ''mi''-''sol''-''do'' est noté ''mi''<sup>6</sup> ;
* le second renversement ''sol''-''do''-''mi'' est noté ''sol''<sup>6</sup><sub>4</sub>.
Il y a une exception : l'accord construit sur la sensible (7{{e}} degré) contient une quinte diminuée et non une quinte juste. Le chiffrage est donc différent :
* l'état fondamental ''si''-''ré''-''fa'' est noté ''si''<sup><s>5</s></sup> (cinq barré), « accord de quinte diminuée » ;
* le premier renversement ''ré''-''fa''-''si'' est noté ''ré''<sup>+6</sup><sub>3</sub>, « accord de sixte sensible et tierce » ;
* le second renversement ''fa''-''si''-''ré'' est noté ''fa''<sup>6</sup><sub>+4</sub>, « accord de sixte et quarte sensible ».
Par ailleurs, on ne considère pas qu'il est fondé sur la sensible, mais sur la dominante ; on met donc des guillemets autour du degré, « “V” ». Donc selon l'état, le chiffrage est “V”<sup><s>5</s></sup>, “V”<sup>+6</sup><sub>3</sub> ou “V”<sup>6</sup><sub>+4</sub>.
En notation jazz, on ajoute « dim », « <sup>o</sup> » ou bien « <sup>♭5</sup> » au chiffrage, ici : B dim, B<sup>o</sup> ou B<sup>♭5</sup> pour l'état fondamental. Pour les renversements : B dim/D et B dim/F ; ou bien B<sup>o</sup>/D et B<sup>o</sup>/F ; ou bien B<sup>♭5</sup>/D et B<sup>♭5</sup>/F.
{{clear}}
[[Fichier:Accords basse chiffree basse do fondamental et renverses.svg|vignette|upright=2|Basse chiffrée : accords de quinte, de sixte et de sixte et de quarte ayant pour basse ''do''.]]
Et concernant les accords ayant pour basse ''do'' en tonalité de ''do'' majeur :
* l'accord ''do''<sup>5</sup> est un accord à l'état fondamental, c'est donc l'accord ''do''-''mi''-''sol'' (sa fondamentale est ''do'') ;
* l'accord ''do''<sup>6</sup> est le premier renversement d'un accord, c'est donc l'accord ''do''-''mi''-''la'' (sa fondamentale est ''la'') ;
* l'accord ''do''<sup>6</sup><sub>4</sub> est le second renversement d'un accord, c'est donc l'accord ''do''-''fa''-''la'' (sa fondamentale est ''fa'').
{{clear}}
== Notes étrangères ==
La musique européenne s'appuie essentiellement sur des accords parfaits, c'est-à-dire fondés sur une tierce majeure ou mineure, et une quinte juste. Il arrive fréquemment qu'un accord ne soit pas un accord parfait. Les notes qui font partie de l'accord parfait sont appelées « notes naturelles » et la note qui n'en fait pas partie est appelée « note étrangère ».
Il existe plusieurs types de notes étrangères :
* anticipation : la note étrangère est une note naturelle de l'accord suivant ;
* appogiature : note d'ornementation qui se résout par mouvement conjoint, c'est-à-dire qu'elle est suivie par une note située juste au-dessus ou en dessous (seconde ascendante ou descendante) qui est, elle, une note naturelle ;
* broderie : on part d'une note naturelle, on monte ou on descend d'une seconde, puis on revient sur la note naturelle ;
* double broderie : on part d'une note naturelle, on joue la note du dessus puis la note du dessous avant de revenir à la note naturelle ; ou bien on joue la note du dessous puis la note du dessus ;
* échappée : note étrangère n'appartenant à aucune des autres catégories ;
* note de passage : mouvement conjoint allant d'une note naturelle d'un accord à une note naturelle de l'accord suivant ;
* pédale : la note de basse reste la même pendant plusieurs accords successifs ;
* retard : la note étrangère est une note naturelle de l'accord précédent.
Les notes étrangères ne sont pas chiffrées.
[[File:Notes etrangeres accords.svg|center|Différents types de notes étrangères.]]
{{note|Les anglophones distinguent deux types de retard : la ''{{lang|en|suspension}}'' est résolue vers le haut (le mouvement est ascendant), le ''{{lang|en|retardation}}'' est résolu vers le bas (le mouvement est descendant).}}
== Principaux accords ==
Les trois principaux accords sont :
* l'accord parfait majeur : il est construit sur les degrés {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (médiante) et {{Times New Roman|V}} (dominante) d'une gamme majeure ; il est noté {{Times New Roman|I}}<sup>5</sup>, {{Times New Roman|IV}}<sup>5</sup>, {{Times New Roman|V}}<sup>5</sup> ;
* l'accord parfait mineur : il est construit sur les degrés {{Times New Roman|I}} (tonique) et {{Times New Roman|IV}} (sous-tonique) d'une gamme mineure harmonique ; il est également noté {{Times New Roman|I}}<sup>5</sup> et {{Times New Roman|IV}}<sup>5</sup>, les anglo-saxons le notent {{Times New Roman|i}}<sup>5</sup> et {{Times New Roman|iv}}<sup>5</sup> (la minuscule indiquant le caractère mineur) ;
* l'accord de septième de dominante : il est construit sur le degré {{Times New Roman|V}} (dominante) d'une gamme majeure ou mineure harmonique ; il est noté {{Times New Roman|V}}<sup>7</sup><sub>+</sub>.
On peut trouver ces trois accords sur d'autres degrés, et il existe d'autre types d'accords. Nous verrons cela plus loin.
{| class="wikitable"
|+ Constitution des principaux accords — Dénomination classique
|-
! scope="col" | Accord
! scope="col" | 1<sup>er</sup> intervalle
! scope="col" | 2<sup>e</sup> intervalle
! scope="col" | 3<sup>e</sup> intervalle
|-
! scope="row" | Accord parfait majeur
| tierce majeure (3M) || quinte juste (5J) || —
|-
! scope="row" | Accord parfait mineur
| tierce mineure (3m) || quinte juste (5J) || —
|-
! scope="row" | Accord de septième de dominante
| tierce majeure (3M) || quinte juste (5J) || septième mineure (7m)
|}
{| class="wikitable"
|+ Constitution des principaux accords — Dénomination jazz
|-
! scope="col" | Accord
! scope="col" | 1<sup>er</sup> intervalle
! scope="col" | 2<sup>e</sup> intervalle
! scope="col" | 3<sup>e</sup> intervalle
|-
! scope="row" | Triade majeure
| tierce majeure (3M) || quinte juste (5J) || —
|-
! scope="row" | Triade mineure
| tierce mineure (3m) || quinte juste (5J) || —
|-
! scope="row" | Accord de septième
| tierce majeure (3M) || quinte juste (5J) || septième mineure (7m)
|}
{| border="0"
|-
| [[Fichier:Accord do majeur arpege puis plaque.midi | Accord parfait de ''do'' majeur (C).]] || [[Fichier:Accord do mineur arpege puis plaque.midi | Accord parfait de ''do'' mineur (Cm).]] || [[Fichier:Accord do septieme arpege puis plaque.midi | Accord de septième de dominante de ''fa'' majeur (C<sup>7</sup>).]]
|-
| Accord parfait<br /> de ''do'' majeur (C). || Accord parfait<br /> de ''do'' mineur (Cm). || Accord de septième de dominante<br /> de ''fa'' majeur (C<sup>7</sup>).
|}
'''Rappel :'''
* la tierce mineure est composée d'un ton et demi (1 t ½) ;
* la tierce majeur est composée de deux tons (2 t) ;
* la quinte juste a la même altération que la fondamentale, sauf lorsque la fondamentale est ''si'' (la quinte juste est alors ''fa''♯) ;
* la septième mineure est le renversement de la seconde majeure (1 t).
[[File:Renversements accords pft fa maj basse chiffree.svg|thumb|Renversements de l'accord parfait de ''fa'' majeur, et la notation de basse chiffrée.]]
[[File:Renversements accord sept de dom fa maj basse chiffree.svg|thumb|Renversements de l'accord de septième de dominante de ''fa'' majeur, et la notation de basse chiffrée.]]
{| class="wikitable"
|+ Notation des principaux accords en musique classique
|-
! scope="col" | Accord
! scope="col" | État<br /> fondamental
! scope="col" | Premier<br /> renversement
! scope="col" | Deuxième<br /> renversement
! scope="col" | Troisième<br /> renversement
|-
! scope="row" | Accord parfait
| {{Times New Roman|I<sup>5</sup>}}<br/> acc. de quinte || {{Times New Roman|I<sup>6</sup>}}<br :> acc. de sixte || {{Times New Roman|I<sup>6</sup><sub>4</sub>}}<br /> acc. de quarte et de sixte || —
|-
! scope="row" | Accord de septième<br /> de dominante
| {{Times New Roman|V<sup>7</sup><sub>+</sub>}}<br /> acc.de septième de dominante || {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}<br />acc. de sixte et quinte diminuée || {{Times New Roman|V<sup>+6</sup>}}<br />acc. de sixte sensible || {{Times New Roman|V<sup>+4</sup>}}<br />acc. de quarte sensible<br />acc. de triton
|}
{| class="wikitable"
|+ Notation des principaux accords en jazz
|-
! scope="col" | Accord
! scope="col" | Chiffrage
! scope="col" | Renversements
|-
! scope="row" | Triade majeure
| X
| rowspan="3" | Les renversements se notent en mettant la basse après une barre de fraction, par exemple pour la triade de ''do'' majeur :
* état fondamental : C ;
* premier renversement : C/E ;
* second renversement : C/G.
|-
! scope="row" | Triade mineure
| Xm, X–
|-
! scope="row" | Septième
| X<sup>7</sup>
|}
{{clear}}
Dans le cas d'un accord de septième de dominante, le nom de l'accord change selon que l'on est en musique classique ou en jazz : en musique classique, on donne le nom de la tonalité alors qu'en jazz, on donne le nom de la fondamentale. Ainsi, l'accord appelé « septième de dominante de ''do'' majeur » en musique classique, est appelé « ''sol'' sept » (G<sup>7</sup>) en jazz : la dominante (degré {{Times New Roman|V}}, dominante) de la tonalité de ''do'' majeur est la note ''sol''.
Comment appelle-t-on en musique classique l'accord appelé « ''do'' sept » (C<sup>7</sup>) en jazz ? Les tonalités dont le ''do'' est la dominante sont les tonalités de ''fa'' majeur (''si''♭ à la clef) et de ''fa'' mineur harmonique (''si''♭, ''mi''♭, ''la''♭ et ''ré''♭ à la clef et ''mi''♮ accidentel). Il s'agit donc de l'accord de septième de dominante des tonalités de ''fa'' majeur et ''fa'' mineur harmonique.
{| class="wikitable"
|+ Accords fréquents pour quelques la tonalités majeures
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Accord parfait<br />{{Times New Roman|I<sup>5</sup>}}
! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}}
|-
|''Do'' majeur || || C<br />''do-mi-sol'' || G7<br />''sol-si-ré-fa''
|-
|''Sol'' majeur || ''fa''♯ || G<br />''sol-si-ré'' || D7<br />''ré-fa''♯''-la-do''
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || D<br />''ré-fa''♯''-la'' || A7<br />''la-do''♯''-mi-sol''
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A<br />''la-do''♯''-mi'' || E7<br />''mi-sol''♯''-si-ré''
|-
| ''Fa'' majeur || ''si''♭ || F<br />''fa-la-do'' || C7<br />''do-mi-sol-si''♭
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭<br />''si''♭''-ré-fa'' || F7<br />''fa-la-do-mi''♭
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭<br />''mi''♭''-sol-si''♭ || B♭7<br />''si''♭''-ré-fa-la''♭
|}
{| class="wikitable"
|+ Accords fréquents pour quelques la tonalités mineures harmoniques
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Accord parfait<br />{{Times New Roman|i<sup>5</sup>}}
! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}}
|-
|''La'' mineur<br />harmonique || || Am, A–<br />''la-do-mi'' || E7<br />''mi-sol''♯''-si-ré''
|-
|''Mi'' mineur<br />harmonique || ''fa''♯ || Em, E–<br />''mi-sol-si'' || B7<br />''si-ré''♯''-fa''♯''-la''
|-
|''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || Bm, B–<br />''si-ré-fa''♯ || F♯7<br />''fa''♯''la''♯''-do''♯''-mi''
|-
|''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || F♯m, F♯–<br />''fa''♯''-la-do''♯ || C♯7<br />''do''♯''-mi''♯''-sol''♯''-si''
|-
| ''Ré'' mineur<br />harmonique || ''si''♭ || Dm, D–<br />''ré-fa-la'' || A7<br />''la-do''♯''-mi-sol''
|-
| ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || Gm, G–<br />''sol-si''♭''-ré'' || D7<br />''ré-fa''♯''-la-do''
|-
| ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || Cm, C–<br />''do-mi''♭''-sol'' || G7<br />''sol-si''♮''-ré-fa''
|}
{{clear}}
== Accords sur les degrés d'une gamme ==
=== Harmonisation d'une gamme ===
[[Fichier:Accord trois notes gamme do majeur chiffre.svg|vignette|upright=1.2|Accords de trois note sur la gamme de ''do'' majeur, chiffrés.]]
On peut ainsi construire une triade par degré d'une gamme.
Pour une gamme majeure, les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|V<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|II<sup>5</sup>}}, {{Times New Roman|III<sup>5</sup>}}, {{Times New Roman|VI<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|ii<sup>5</sup>}}, {{Times New Roman|iii<sup>5</sup>}}, {{Times New Roman|vi<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}.
Les accords ont tous une quinte juste à l'exception de l'accord {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} qui a une quinte diminuée, raison pour laquelle le « 5 » est barré. C'est un accord dit « de quinte diminuée ». En jazz, l'accord diminué est noté « dim », « ° », « m<sup>♭5</sup> » ou « <sup>–♭5</sup> ».
Nous avons donc trois types d'accords (dans la notation jazz) : X (triade majeure), Xm (triade mineure) et X° (triade diminuée), la lettre X remplaçant le nom de la note fondamentale.
{{clear}}
[[Fichier:Accord trois notes gamme la mineur chiffre.svg|vignette|upright=1.2|Accords de trois notes sur une gamme de ''la'' mineur harmonique, chiffrés.]]
Pour une gamme mineure harmonique, les accords {{Times New Roman|III<sup>+5</sup>}}, {{Times New Roman|V<sup>♯</sup>}} et {{Times New Roman|VI<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|II<sup><s>5</s></sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|i<sup>5</sup>}}, {{Times New Roman|ii<sup><s>5</s></sup>}}, {{Times New Roman|iv<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}.
Les accords {{Times New Roman|ii<sup><s>5</s></sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} ont une quinte diminuée ; ce sont des accords dits « de quinte diminuée ». L'accord {{Times New Roman|III<sup>+5</sup>}} a une quinte augmentée ; le signe « plus » indique que la note de cinquième, le ''sol'' dièse, est la sensible. En jazz, l'accord est noté « aug » ou « <sup>+</sup> ». Les autres accords ont une quinte juste.
Aux trois accords générés par une gamme majeure (X, Xm et X°), nous voyons ici apparaître un quatrième type d'accord : la triade augmentée X<sup>+</sup>.
Nous remarquons que des gammes ont des accords communs. Par exemple, l'accord {{Times New Roman|ii<sup>5</sup>}} de ''do'' majeur est identique à l'accord {{Times New Roman|iv<sup>5</sup>}} de ''la'' mineur (il s'agit de l'accord Dm).
Quel que soit le mode, les accords construits sur la sensible (accord de quinte diminuée) sont rarement utilisés. S'ils le sont, c'est en tant qu'accord de septième de dominante sans fondamentale (voir ci-après). C'est la raison pour laquelle le chiffrage indique le degré {{Times New Roman|V}} entre guillemets, et non pas le degré {{Times New Roman|VII}} (mais pour des raisons de clarté, nous l'indiquons entre parenthèses au début).
En mode mineur, l'accord de quinte augmentée {{Times New Roman|iii<sup>+5</sup>}} est très peu utilisé (voir plus loin ''[[#Progression_d'accords|Progression d'accords]]''). C'est un accord considéré comme dissonant.
On voit que :
* un accord parfait majeur peut appartenir à cinq gammes différentes ;<br /> par exemple l'accord parfait de ''do'' majeur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> degré de la gamme de ''do'' majeur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' mineur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''mi'' mineur ;
* un accord parfait mineur peut appartenir à cinq gammes différentes ;<br />par exemple l'accord parfait de ''la'' mineur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> de la gamme de ''la'' mineur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''mi'' mineur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|III}}<sup>e</sup> degré de ''fa'' majeur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''do'' majeur ;
* un accord de quinte diminuée peut appartenir à trois gammes différentes ;<br />par exemple, l'accord de quinte diminuée de ''si'' est l'accord construit sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' majeur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''la'' mineur et sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' mineur ;
* un accord de quinte augmentée (à l'état fondamental) ne peut appartenir qu'à une seule gamme ;<br /> par exemple, l'accord de quinte augmentée de ''do'' est l'accord construit sur le {{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur.
{| class="wikitable"
|+ Notation jazz des triades
|-
| rowspan="2" colspan="2" |
! scope="col" colspan="2" | Tierce
|-
! scope="col" | 3m
! scope="col" | 3M
|-
! rowspan="3" | Quinte
! scope="row" | 5d
| Xᵒ, X<sub>m</sub><sup>(♭5)</sup> ||
|-
! scope="row" | 5J
| Xm, X– || X
|-
! scope="row" | 5A
| || X+, X<sup>(♯5)</sup>
|}
=== Harmonisation par des accords de septième ===
[[Fichier:Harmonisation gamme do majeur par septiemes chiffre.svg|vignette|upright=2|Harmonisation de la gamme de do majeur par des accords de septième.]]
Les accords de septième contiennent une dissonance et créent ainsi une tension. Ils sont très utilisés en jazz. Nous avons représenté ci-contre l'harmonisation de la gamme de ''do'' majeur.
La constitution des accords est la suivantes :
* tierce majeure (3M)
** quinte juste (5J)
*** septième mineure (7m) : sur le degré V, c'est l'accord de septième de dominante V<sup>7</sup><sub>+</sub>, noté X<sup>7</sup> (X pour G),
*** septième majeure (7M) : sur les degrés I et IV, appelés « accords de septième majeure » et notés aussi X<sup>maj7</sup> ou X<sup>Δ</sup> (X pour C ou F) ;
* tierce mineure (3m)
** quinte juste (5J)
*** septième mineure : sur les degrés ii, iii et vi, appelés « accords mineur septième » et notés Xm<sup>7</sup> ou X–<sup>7</sup> (X pour D, E ou A),
** quinte diminuée (5d)
*** septième mineure (7m) : sur le degré vii, appelé « accord demi-diminué » (puisque seule la quinte est diminuée) et noté X<sup>∅</sup> ou Xm<sup>7(♭5)</sup> ou X–<sup>7(♭5)</sup> (X pour B) ;<br /> en musique classique, on considère que c'est un accord de neuvième de dominante sans fondamentale.
Nous avons donc quatre types d'accords : X<sup>7</sup>, X<sup>maj7</sup>, Xm<sup>7</sup> et X<sup>∅</sup>
En jazz, on ajoute souvent la quarte à l'accord de sous-dominante IV (sur le ''fa'' dans une gamme de ''do'' majeur) ; il s'agit ici d'une quarte augmentée (''fa''-''si'') et l'accord est surnommé « accord lydien » mais cette dénomination est erronée (il s'agit d'une mauvaise interprétation de textes antiques). C'est un accord de onzième sans neuvième (la onzième étant l'octave de la quarte), il est noté X<sup>maj7(♯11)</sup> ou X<sup>Δ(♯11)</sup> (ici, F<sup>maj7(♯11)</sup>, ''fa''-''la''-''do''-''mi''-''si'' ou ''fa''-''la''-''si''-''do''-''mi'').
{| class="wikitable"
|+ Chiffrage jazz des accords de septième
|-
! scope="col" rowspan="2" | Tierce
! scope="col" rowspan="2" | Quinte
! scope="col" colspan="2" | Septième
|-
! scope="col" | 7m
! scope="col" | 7M
|-
| rowspan="2" | 3m
| 5d || 7m || X<sup>∅</sup>, Xm<sup>7(♭5)</sup>
|-
| rowspan="2" | 5J
| X<sub>m</sub><sup>7</sup> ||
|-
| rowspan="2" | 3M
| X<sup>7</sup> || X<sup>maj7</sup>, X<sup>Δ</sup>
|-
| 5A || || X<sub>+</sub><sup>maj7</sup>, X<sub>+</sub><sup>Δ</sup>
|}
Note : le « m » peut être remplacé par un signe moins « – ».
=== Modulation et emprunt ===
Un morceau peut comporter des changements de tonalité; appelés « modulation ». Il y a parfois un court passage dans une tonalité différente, typiquement sur une ou deux mesures, avant de retourner dans la tonalité d'origine : on parle d'emprunt. Lorsqu'il y a une modulation ou un emprunt, les degrés changent. Un même accord peut donc avoir une fonction dans une partie du morceau et une autre fonction ailleurs. L'utilisation d'accord différents, et en particulier d'accord utilisant des altérations accidentelles, indique clairement une modulation.
Nous avons vu précédemment que les modulations courantes sont :
* les modulations dans les tons voisins ;
* les modulations homonymes ;
* les marches harmoniques.
Une modulation entre une tonalité majeure et mineure change la couleur du passage,
* la modulation la plus « douce » est entre les tonalités relatives (par exemple''do'' majeur et ''la'' mineur) car ces tonalités utilisent quasiment les mêmes notes ;
* la modulation la plus « voyante » est la modulation homonyme (par exemple entre ''do'' majeur et ''do'' mineur).
Une modulation commence souvent sur l'accord de dominante de la nouvelle tonalité.
Pour analyser un œuvre, ou pour improviser sur une partie, il est important de reconnaître les modulations. La description de la successind es tonalités s'appelle le « parcours tonal ».
=== Exercices élémentaires ===
L'apprentissage des accords passe par quelques exercices élémentaires.
'''1. Lire un accord'''
Il s'agit de lecture de notes : des notes composant les accords sont écrites « empilées » sur une portée, il faut les lire en énonçant les notes de bas en haut.
'''2. Reconnaître la « couleur » d'un accord'''
On écoute une triade et il faut dire si c'est une triade majeure ou mineure. Puis, on complexifie l'exercice en ajoutant la septième.
'''3. Chiffrage un accord'''
Trouver le nom d'un accord à partir des notes qui le composent.
'''4. Réalisation d'un accord'''
Trouver les notes qui composent un accord à partir de son nom.
'''5. Dictée d'accords'''
On écoute une succession d'accords et il faut soit écrire les notes sur une portée, soit écrire les noms de accords.
[[File:Exercice constitution accord basse chiffree.svg|thumb|Exercice : constitution d'accord à partir de la basse chiffrée.]]
'''Exercices de basse chiffrée'''
''Réalisation d'un accord''
Sur la figure suivante, écrire les notes des accords correspondant à la basse chiffrée. Déterminer le degré de la fondamentale pour chaque accord en considérant que nous sommes dans la tonalité de ''sol'' majeur.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice constitution accord basse chiffree solution.svg|vignette|Solution.]]
# La note de basse est un ''do''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''mi'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''sol''.<br />Le chiffrage « <sup>5</sup> » indique que c'est un accord dans son état fondamental (l'écart entre deux notes consécutives ne dépasse pas la tierce), la fondamentale est donc la basse, ''do'', qui est le degré IV de la tonalité.
# La note de basse est un ''si''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''ré'', puis nous appliquons le chiffrage 6 et ajoutons la sixte, ''sol''.<br />Le chiffrage « <sup>6</sup> » indique que c'est un accord dans son premier renversement. En le remettant dans son état fondamental, nous obtenons ''sol-si-ré'', la fondamentale est donc la tonique, le degré I.
# La note de basse est un ''la''. Nous ajoutons la tierce (chiffre 3), ''do'', et la sixte (6), ''fa''♯. Nous vérifions que le ''fa''♯ est la sensible (signe +)<br />Nous voyons un « blanc » entre les notes ''do'' et ''fa''♯. En descendant le ''fa''♯ à l'octave inférieure, nous obtenons un empilement de tierces ''fa''♯-''la-do'', le fondamentale est donc ''fa''♯, le degré VII. Nous pouvons le voir comme le deuxième renversement de l'accord de septième de dominante, sans fondamentale.
# La note de basse est un ''fa''♯. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''la'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''do'' ; nous vérifions qu'il s'agit bien d'une quinte diminuée (le 5 est barré). Nous appliquons le chiffre 6 et ajoutons la sixte, ''ré''.<br />Nous voyons que les notes ''do'' et ''ré'' sont conjointes (intervalle de seconde). En descendant le ''ré'' à l'octave inférieure, nous obtenons un empilement de tierces ''ré-fa''♯-''la-do'', le fondamentale est donc ''ré'', le degré V. Nous constatons que l'accord chiffré est le premier renversement de l'accord de septième de dominante.
{{boîte déroulante/fin}}
{{clear}}
''Chiffrage d'accords''
[[Fichier:Exercice chiffrage accord basse chiffree.svg|vignette|Accords à chiffrer.]]
Chiffrer les accords ci-contre.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice chiffrage accord basse chiffree solution.svg|vignette|Solution.]]
# On relève les intervalles en partant de la basse : tierce majeure (3M) et quinte juste (5J). Le chiffrage complet est donc ''fa''<sup>5</sup><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''fa''<sup>5</sup>.<br /> On peut aussi reconnaître que c'est l'accord parfait sur la tonique de la tonalité de ''fa'' majeur dans son état fondamental, le chiffrage d'un accord parfait étant <sup>5</sup>.
# On relève les intervalles en partant de la basse : quarte juste (4J), sixte majeure (6M). Le chiffrage complet est donc ''fa''<sup>6</sup><sub>4</sub>.<br /> On peut aussi reconnaître que c'est le second renversement de l'accord ''mi-sol-si'', sur la tonique de la tonalité de ''mi'' mineur, le chiffrage du second renversement d'un accord parfait étant <sup>6</sup><sub>4</sub>.
# Les intervalles en partant de la basse sont : tierce mineure (3m), quinte diminuée (5d), sixte mineure (6m). Le chiffrage complet est donc ''mi''<sup>6</sup><small><s>5</s></small><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''mi''<sup>6</sup><sub><s>5</s></sub>.<br /> On reconnaît le premier renversement de l'accord ''do-mi-sol-si''♭, accord de septième de dominante de la tonalité de ''fa'' majeur.
# Les intervalles en partant de la basse sont : tierce mineure (3m), quinte juste (5J), septième mineure (7m). Le chiffrage complet est donc ''ré''<sup>7</sup><small>5</small><sub>3</sub> ; c'est typique d'un accord de septième de dominante, son chiffrage est donc ''ré''<sup>7</sup><sub>+</sub>.<br /> On reconnaît l'accord de septième de dominante de la tonalité de ''sol'' mineur dans son état fondamental.
{{boîte déroulante/fin}}
{{clear}}
[[File:Exercice constitution accord notation jazz.svg|thumb|Exercice : constitution d'un accord d'après son chiffrage en notation jazz.]]
'''Exercices de notation jazz'''
''Réalisation d'un accord''
Sur la figure suivante, écrire les notes des accords correspondant aux chiffrages.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice constitution accord notation jazz solution.svg|thumb|Solution.]]
# Il s'agit de la triade majeure de ''do'' dans son état fondamental. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''do-mi-sol''.
# Il s'agit de la triade majeure de ''sol''. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''sol-si-ré''. On renverse l'accord afin que la basse soit le ''si'', l'accord est donc ''si-ré-sol''.
# Il s'agit de l'accord demi-diminué de ''fa''♯. Les intervalles sont la tierce mineure (3m), la quinte diminuée (5d) et la septième mineure (7m). Les notes sont donc ''fa''♯-''la-do-mi''. Nous renversons l'accord afin que la basse soit le ''la'', l'accord est donc ''a-do-mi-fa''♯.
# Il s'agit de l'accord de septième de ''ré''. Les intervalles sont donc la tierce majeure (3M), la quinte juste (5J) et la septième mineure (7m). Les notes sont ''ré-fa''♯''-la-do''. Nous renversons l'accord afin que la basse soit le ''fa''♯, l'accord est donc ''fa''♯''-la-do-ré''.
{{boîte déroulante/fin}}
{{clear}}
''Chiffrage d'accords''
[[File:Exercice chiffrage accord notation jazz.svg|thumb|Accords à chiffrer.]]
Chiffrer les accords ci-contre.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice chiffrage accord notation jazz solution.svg|thumb|Solution.]]
# Les notes sont toutes sur des interlignes consécutifs, c'est donc un empilement de tierces ; l'accord est dans son état fondamental. Les intervalles sont une tierce majeure (''fa-la'' : 3M) et une quinte juste (''fa-do'' : 5J), c'est donc la triade majeure de ''fa''. Le chiffrage est F.
# Il y a un blanc dans l'empilement des notes, c'est donc un accord renversé. En permutant les notes pour n'avoir que des tierces, on trouve l'accord ''mi-sol-si''. Les intervalles sont une tierce mineure (''mi-sol'' : 3m) et une quinte juste (''mi-si'' : 5J), c'est donc la triade mineure de ''mi'' avec un ''si'' à la basse. Le chiffrage est Em/B ou E–/B.
# Il y deux notes conjointes, c'est donc un renversement. L'état fondamental de cet accord est ''do-mi-sol-si''♭. Les intervalles sont une tierce majeure (''do-mi'' : 3M), une quinte juste (''do-sol'' : 5J) et une septième mineure (''do-si''♭ : 7m). C'est donc l'accord de ''do'' septième avec un ''mi'' à la basse, chiffré C<sup>7</sup>/E.
# Les notes sont toutes sur des interlignes consécutifs, l'accord est dans son état fondamental. Les intervalles sont la tierce mineure (''ré-fa'' : 3m), une quinte juste (''ré-la'' : 5J) et une septième mineure (''ré-do'' : 7m). C'est donc l'accord de ''ré'' mineur septième, chiffré Dm<sup>7</sup> ou D–<sup>7</sup>.
{{boîte déroulante/fin}}
{{clear}}
== Harmonie fonctionnelle ==
Le choix des accords et de leur succession — la progression des accords — est un élément important d'un morceau, de sa composition. Le compositeur ou la compositrice a bien sûr une liberté totale, mais pour faire des choix, il faut comprendre les conséquences de ces choix, et donc ici, les effets produits par les accords et leur progression.
Une des manières d'aborder le sujet est l'harmonie fonctionnelle.
=== Les trois fonctions des accords ===
En harmonie tonale, on considère que les accords ont une fonction. Il existe trois fonctions :
* la fonction de tonique, {{Times New Roman|I}} ;
* la fonction de sous-dominante, {{Times New Roman|IV}} ;
* la fonction de dominante, {{Times New Roman|V}}.
L'accord de tonique, {{Times New Roman|I}}, est l'accord « stable » de la tonalité par excellence. Il conclut en général les morceaux, et ouvre souvent les morceaux ; il revient fréquemment au cours du morceau.
L'accord de dominante, {{Times New Roman|V}}, est un accord qui introduit une instabilité, une tension. En particulier, il contient la sensible (degré {{Times New Roman|VI}}), qui est une note « aspirée » vers la tonique. Cette tension, qui peut être renforcée par l'utilisation d'un accord de septième, est fréquemment résolue par un passage vers l'accord de tonique. Nous avons donc deux mouvements typiques : {{Times New Roman|I}} → {{Times New Roman|V}} (création d'une tension, d'une attente) et {{Times New Roman|V}} → {{Times New Roman|I}} (résolution d'une tension). Les accords de tonique et de dominante ont le cinquième degré en commun, cette note sert donc de pivot entre les deux accords.
L'accord de sous-dominante, {{Times New Roman|IV}}, est un accord qui introduit lui aussi une tension, mais moins grande : il ne contient pas la sensible. Notons que s'il est une quarte au-dessus de la tonique, il est aussi une quinte en dessous d'elle ; il est symétrique de l'accord de dominante. Il a donc un rôle similaire à l'accord de dominante, mais atténué. L'accord de sous-dominante aspire soit vers l'accord de dominante, très proche, et l'on a alors une augmentation de la tension ; soit vers l'accord de tonique, un retour vers la stabilité (il a alors un rôle semblable à la dominante). Du fait de ces deux bifurcations possibles — augmentation de la tension ({{Times New Roman|IV}} → {{Times New Roman|V}}) ou retour à la stabilité ({{Times New Roman|IV}} → {{Times New Roman|I}}) —, l'utilisation de l'accord de sous-dominante introduit un certain flottement : si l'on peut facilement prédire l'accord qui suit un accord de dominante, on ne peut pas prédire ce qui suit un accord de sous-dominante.
Notons que la composition ne consiste pas à suivre ces règles de manière stricte, ce qui conduirait à des morceaux stéréotypés et plats. Le plaisir d'écoute joue sur une alternance entre satisfaction d'une attente (respect des règles) et surprise (rompre les règles).
=== Accords remplissant ces fonctions ===
Les accords sur les autres degrés peuvent se ramener à une de ces trois fonctions :
* {{Times New Roman|II}} : fonction de sous-dominante {{Times New Roman|IV}} ;
* {{Times New Roman|III}} (très peu utilisé en mode mineur en raison de sa dissonance) et {{Times New Roman|VI}} : fonction de tonique {{Times New Roman|I}} ;
* {{Times New Roman|VII}} : fonction de dominante {{Times New Roman|V}}.
En effet, les accords étant des empilements de tierces, des accords situés à une tierce l'un de l'autre — {{Times New Roman|I}} ↔ {{Times New Roman|III}}, {{Times New Roman|II}} ↔ {{Times New Roman|IV}}, {{Times New Roman|V}} ↔ {{Times New Roman|VII}}, {{Times New Roman|VI}} ↔ {{Times New Roman|VIII}} ( = {{Times New Roman|I}}) — ont deux notes en commun. On retrouve le fait que l'accord sur le degré {{Times New Roman|VII}} est considéré comme un accord de dominante sans tonique. En mode mineur, l'accord sur le degré {{Times New Roman|III}} est évité, il n'a donc pas de fonction.
{|class="wikitable"
|+ Fonction des accords
|-
! scope="col" | Fondamentale
! scope="col" | Fonction
|-
| {{Times New Roman|I}} || tonique
|-
| {{Times New Roman|II}} || sous-dominante faible
|-
| {{Times New Roman|III}} || tonique faible
|-
| {{Times New Roman|IV}} || sous-dominante
|-
| {{Times New Roman|V}} || dominante
|-
| {{Times New Roman|VI}} || tonique faible
|-
| {{Times New Roman|VII}} || dominante faible
|}
Par exemple en ''do'' majeur :
* fonction de tonique : '''''do''<sup>5</sup> (C)''', ''mi''<sup>5</sup> (E–), ''la''<sup>5</sup> (A–) ;
* fonction de sous-dominante : '''''fa''<sup>5</sup> (F)''', ''ré''<sup>5</sup> (D–) ;
* fonction de dominante : '''''sol''<sup>5</sup> (G)''' ou ''sol''<sup>7</sup><sub>+</sub> (G<sup>7</sup>), ''si''<sup> <s>5</s></sup> (B<sup>o</sup>).
En ''la'' mineur harmonique :
* fonction de tonique : '''''la''<sup>5</sup> (A–)''', ''fa''<sup>5</sup> (F) [, rarement : ''do''<sup>+5</sup> (C<sup>+</sup>)] ;
* fonction de sous-dominante : '''''ré''<sup>5</sup> (D–)''', ''si''<sup> <s>5</s></sup> (B<sup>o</sup>) ;
* fonction de dominante : '''''mi''<sup>5</sup> (E)''' ou ''mi''<sup>7</sup><sub>+</sub> (E<sup>7</sup>), ''sol''♯<sup> <s>5</s></sup> (G♯<sup>o</sup>).
Le fait d'utiliser des accords différents pour remplir une fonction permet d'enrichir l'harmonie, et de jouer sur l'équilibre entre satisfaction d'une attente (on respecte les règles sur les fonctions) et surprise (mais on n'utilise pas l'accord attendu).
=== Les dominantes secondaires ===
On utilise aussi des accords de septième dominante se fondant sur un autre degré que la dominante de la gamme ; on parle de « dominante secondaire ». Typiquement, avant un accord de septième de dominante, on utilise parfois un accord de dominante de dominante, dont le degré est alors noté « {{Times New Roman|V}} de {{Times New Roman|V}} » ou « {{Times New Roman|V}}/{{Times New Roman|V}} » ; la fondamentale est de l'accord est alors situé cinq degrés au-dessus de la dominante ({{Times New Roman|V}}), c'est donc le degré {{Times New Roman|IX}}, c'est-à-dire le degré {{Times New Roman|II}} de la tonalité en cours). Ou encore, on utilise un accord de dominante du degré {{Times New Roman|IV}} (« {{Times New Roman|V}} de {{Times New Roman|IV}} », la fondamentale est alors le degré {{Times New Roman|I}}) avant un accord sur le degré {{Times New Roman|IV}} lui-même.
Par exemple, en tonalité de ''do'' majeur, on peut trouver un accord ''ré - fa''♯'' - la - do'' (chiffré {{Times New Roman|V}} de {{Times New Roman|V}}<sup>7</sup><sub>+</sub>), avant un accord ''sol - si - ré - fa'' ({{Times New Roman|V}}<sup>7</sup><sub>+</sub>). L'accord ''ré - fa''♯'' - la - do'' est l'accord de septième de dominante des tonalités de ''sol''. Dans la même tonalité, on pourra utiliser un accord ''do - mi - sol - si''♭ ({{Times New Roman|V}} de {{Times New Roman|IV}}<sup>7</sup><sub>+</sub>) avant un accord ''fa - la - do'' ({{Times New Roman|IV}}<sup>5</sup>). Le recours à une dominante secondaire peut atténuer une transition, par exemple avec un enchaînement ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> → ''fa''<sup>5</sup> (C → C<sup>7</sup> → F) qui correspond à un enchaînement {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}} : le passage ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> (C → C<sup>7</sup>) se fait en ajoutant une note (le ''si''♭) et rend naturel le passage ''do'' → ''fa''.
Sur les sept degré de la gamme, on ne considère en général que cinq dominantes secondaires : en effet, la dominante du degré {{Times New Roman|I}} est la dominante « naturelle, primaire » de la tonalité (et n'est donc pas secondaire) ; et utiliser la dominante de {{Times New Roman|VII}} consisterait à considérer l'accord de {{Times New Roman|VII}} comme un accord propre, on évite donc les « {{Times New Roman|V}} de “{{Times New Roman|V}}” » (mais les « “{{Times New Roman|V}}” de {{Times New Roman|V}} » sont tout à fait « acceptables »).
=== Enchaînements classiques ===
Nous avons donc vu que l'on trouve fréquemment les enchaînements suivants :
* pour créer une instabilité :
** {{Times New Roman|I}} → {{Times New Roman|V}},
** {{Times New Roman|I}} → {{Times New Roman|IV}} (instabilité moins forte mais incertitude sur le sens d'évolution) ;
* pour maintenir l'instabilité :
** {{Times New Roman|IV}} → {{Times New Roman|V}} ;
* pour résoudre l'instabilité :
** {{Times New Roman|IV}} → {{Times New Roman|I}},
** {{Times New Roman|V}} → {{Times New Roman|I}}, cas particuliers (voir plus bas) :
*** {{Times New Roman|V}}<sup>+4</sup> → {{Times New Roman|I}}<sup>6</sup>,
*** {{Times New Roman|I}}<sup>6</sup><sub>4</sub> → {{Times New Roman|V}}<sup>7</sup><sub>+</sub> → {{Times New Roman|I}}<sup>5</sup>.
Les degrés indiqués ci-dessus sont les fonctions ; on peut donc utiliser les substitutions suivantes :
* {{Times New Roman|I}} par {{Times New Roman|VI}} et, en tonalité majeure, {{Times New Roman|III}} ;
* {{Times New Roman|IV}} par {{Times New Roman|II}} ;
* {{Times New Roman|V}} par {{Times New Roman|VII}}.
Pour enrichir l'harmonie, on peut utiliser les dominantes secondaires, en particulier :
* {{Times New Roman|V}} de {{Times New Roman|V}} ({{Times New Roman|II}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|V}},
* {{Times New Roman|V}} de {{Times New Roman|IV}} ({{Times New Roman|I}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|IV}}.
On peut enchaîner les enchaînements, par exemple {{Times New Roman|I}} → {{Times New Roman|IV}} → {{Times New Roman|V}}, ou encore {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}}… En jazz, on utilise très fréquemment l'enchaînement {{Times New Roman|II}} → {{Times New Roman|V}} → {{Times New Roman|I}} (deux-cinq-un).
On peut bien sûr avoir d'autres enchaînements, mais ces règles permettent d'analyser un grand nombre de morceaux, et donnent des clefs utiles pour la composition. Nous voyons ci-après un certain nombre d'enchaînements courants dans différents styles
== Exercice ==
Un hautboïste travaille la sonate en ''do'' mineur S. 277 de Heinichen. Sur le deuxième mouvement ''Allegro'', il a du mal à travailler un passage en raison des altérations accidentelles. Sur la suggestion de sa professeure, il décide d'analyser la progression d'accords sous-jacente afin que les altérations deviennent logiques. Il s'agit d'un duo hautbois et basson pour lequel les accords ne sont pas chiffrés, le basson étant ici un instrument soliste et non pas un élément de la basse continue.
Sur l'extrait suivant, déterminez les basses et la qualité (chiffrage) des accords sous-jacents. Commentez.
[[Fichier:Sonate hautbois basson heinichen 2e mvt mes49.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen.]]
{{note|L'œuvre est en ''do'' mineur et devrait donc avoir trois bémols à la clef, or ici il n'y en a que deux. En effet, le ''la'' pouvant être bécarre en mode mineur mélodique ascendant, le compositeur a préféré le noter explicitement en altération accidentelle lorsque l'on est en mode mélodique naturel, harmonique ou mélodique descendant. C'est un procédé assez courant à l'époque baroque.}}
{{boîte déroulante/début|titre=Solution}}
Une des difficultés ici est que les arpèges joués par les instruments sont agrémentés de notes de passage.
Les notes de la basse (du basson) sont différentes entre le premier et le deuxième temps de chaque mesure et ne peuvent pas appartenir au même accord. On a donc un accord par temps.
Sur le premier temps de chaque mesure, le basson joue une octave. La note concernée est donc la basse de chaque accord. Pour savoir s'il s'agit d'un accord à l'état fondamental ou d'un renversement, on regarde ce que joue le hautbois : dans un mouvement conjoint (succession d'intervalles de secondes), il est difficile de distinguer les notes de l'arpège des notes de passage, mais
: les notes des grands intervalles font partie de l'accord.
Ainsi, sur le premier temps de la première mesure (la basse est un ''mi''♭), on a une sixte descendante ''sol''-''si''♭ et, à la fin du temps, une tierce descendante ''sol''-''mi''♭. L'accord est donc ''mi''♭-''sol''-''si''♭, c'est un accord de quinte (accord parfait à l'état fondamental). À la fin du premier temps, le basson joue un ''do'', c'est donc une note étrangère.
Sur le second temps de la première mesure, le basson joue une tierce ascendante ''fa''-''la''♭, la première note est la basse de l'accord et la seconde une des notes de l'accord. Le hautbois commence par une sixte descendante ''la''♭-''do'', l'accord est donc ''fa''-''la''♭-''do'', un accord de quinte (accord parfait à l'état fondamental). Le ''do'' du basson la fin du premier temps est donc une anticipation.
Les autres notes étrangères de la première mesure sont des notes de passage.
Mais il faut faire attention : en suivant ce principe, sur les premiers temps des deuxième et troisième mesure, nous aurions des accords de septième d'espèce (puisque la septième est majeure). Or, on ne trouve pas, ou alors exceptionnellement, d'accord de septième d'espèce dans le baroque, mais quasi exclusivement des accords de septième de dominante. Donc au début de la deuxième mesure, le ''la''♮ est une appoggiature du ''si''♭, l'accord est donc ''si''♭-''ré''-''fa'', un asscord de quinte. De même, au début de la troisième mesure, le ''sol'' est une appoggiature du ''la''♭.
Il faut donc se méfier d'une analyse purement « mathématique ». Il faut s'attacher à ressentir la musique, et à connaître les styles, pour faire une analyse pertinente.
Ci-dessous, nous avons grisé les notes étrangères.
[[Fichier:Sonate hautbois basson heinichen 2e mvt mes49 analyse.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen. Analyse de la progression harmonique.]]
Le chiffrage jazz équivalent est :
: | E♭ F– | B♭<sup>Δ</sup> E♭ | A♭<sup>Δ</sup> D– | G …
Nous remarquons une progression assez régulière :
: ''mi''♭ ↗[2<sup>de</sup>] ''fa'' | ↘[5<sup>te</sup>] ''si''♭ ↗[4<sup>te</sup>] ''mi''♭ | ↘[5<sup>te</sup>] ''la''♭ ↗[4<sup>te</sup>] ''ré'' | ↘[5<sup>te</sup>] ''sol''
Le ''mi''♭ est le degré {{Times New Roman|III}} de la tonalité principale (''do'' mineur), c'est donc une tonique faible ; il « joue le même rôle » qu'un ''do''. S'il y avait eu un accord de ''do'' au début de l'extrait, on aurait eu une progression parfaitement régulière ↗[4<sup>te</sup>] ↘[5<sup>te</sup>].
Nous avons les modulations suivantes :
* mesure 49 : ''do'' mineur naturel (le ''si''♭ n'est pas une sensible) avec un accord sur “{{Times New Roman|I}}” (tonique faible, {{Times New Roman|III}}, pour la première analyse, ou bien tonique forte, {{Times New Roman|I}}, pour la seconde) suivi d'un accord sur {{Times New Roman|IV}} ;
* mesure 50 : ''si''♭ majeur avec un accord sur {{Times New Roman|I}} suivi d'un accord sur {{Times New Roman|IV}} ;
* mesure 51 : ''la''♭ majeur avec un accord sur {{Times New Roman|I}}, et emprunt à ''do'' majeur avec un accord sur {{Times New Roman|II}} ({{Times New Roman|IV}} faible).
On a donc une marche harmonique {{Times New Roman|I}} → {{Times New Roman|IV}} qui descend d'une seconde majeure (un ton) à chaque mesure (''do'' → ''si''♭ → ''la''♭), avec une exception sur la dernière mesure (modulation en cours de mesure et descente d'une seconde mineure au lieu de majeure).
Ce passage est donc construit sur une régularité, une règle qui crée un effet d'attente — enchaînement {{Times New Roman|I}}<sup>5</sup> → {{Times New Roman|IV}}<sup>5</sup> avec une marche harmonique d'une seconde majeure descendante —, et des « surprises », des exceptions au début — ce n'est pas un accord {{Times New Roman|I}}<sup>5</sup> mais un accord {{Times New Roman|III}}<sup>5</sup> — et à la fin — modulation en milieu de mesure et dernière descente d'une seconde mineure (½t ''la''♭ → ''sol'').
L'extrait ne permet pas de le deviner, mais la mesure 52 est un retour en ''do'' mineur, avec donc une modulation sur la dominante (accord de ''sol''<sup>7</sup><sub>+</sub>, G<sup>7</sup>).
{{boîte déroulante/fin}}
== Progression d'accords ==
Comme pour la mélodie, la succession des accords dans un morceau, la progression d'accords, suit des règles. Et comme pour la mélodie, les règles diffèrent d'un style musical à l'autre et la créativité consiste à parfois ne pas suivre ces règles. Et comme pour la mélodie, on part d'un ensemble de notes organisé, d'une gamme caractéristique d'une tonalité, d'un mode.
Les accords les plus utilisés pour une tonalité donnée sont les accords dont la fondamentale sont les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la tonalité, en particulier la triade {{Times New Roman|I}}, appelée « accord parfait » ou « accord de tonique », et l'accord de septième {{Times New Roman|V}}, appelé « septième de dominante ».
Le fait d'avoir une progression d'accords qui se répète permet de structurer un morceau. Pour les morceaux courts, il participe au plaisir de l'écoute et facilite la mémorisation (par exemple le découpage couplet-refrain d'une chanson). Sur les morceaux longs, une trop grande régularité peut introduire de la lassitude, les longs morceaux sont souvent découpés en parties présentant chacune une progression régulière. Le fait d'avoir une progression régulière permet la pratique de l'improvisation : cadence en musique classique, solo en jazz et blues.
; Note
: Le terme « cadence » désigne plusieurs choses différentes, et notamment en harmonie :
:* une partie improvisée dans un opéra ou un concerto, sens utilisé ci-dessus ;
:* une progression d'accords pour ponctuer un morceau et en particulier pour le conclure, sens utilisé dans la section suivante.
=== Accords peu utilisés ===
En mode mineur, l'accord de quinte augmentée {{Times New Roman|III<sup>+5</sup>}} est très peu utilisé. C'est un accord dissonant ; il intervient en général comme appogiature de l'accord de tonique (par exemple en ''la'' mineur : {{Times New Roman|III<sup>+5</sup>}} ''do'' - ''mi'' - ''sol''♯ → {{Times New Roman|I<sup>6</sup>}} ''do'' - ''mi'' - ''la''), ou de l'accord de dominante ({{Times New Roman|III<sup>6</sup><sub>+3</sub>}} ''mi'' - ''sol''♯ - ''do'' → {{Times New Roman|V<sup>5</sup>}} ''mi'' - ''sol''♯ - ''si''). Il peut être aussi utilisé comme préparation à l'accord de sous-dominante (enchaînement {{Times New Roman|III}} → {{Times New Roman|IV}}). Par ailleurs, il a une constitution symétrique — c'est l'empilement de deux tierces majeures — et ses renversements ont les mêmes intervalles à l'enharmonie près (quinte augmentée/sixte mineure, tierce majeure/quarte diminuée). De ce fait, un même accord est commun, par renversement et à l'enharmonie près, à trois tonalités : le premier renversement de l'accord ''do'' - ''mi'' - ''sol''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur) est enharmonique à ''mi'' - ''sol''♯ - ''si''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''do''♯ mineur) ; le second renversement est enharmonique à ''la''♭ - ''do'' - ''mi'' ({{Times New Roman|III}}<sup>e</sup> degré de ''fa'' mineur).
=== Accords très utilisés ===
Les trois accords les plus utilisés sont les accords de tonique (degré {{Times New Roman|I}}), de sous-dominante ({{Times New Roman|IV}}) et de dominante ({{Times New Roman|V}}). Ils interviennent en particulier en fin de phrase, dans les cadences. L'accord de dominante sert souvent à introduire une modulation : la modulation commence sur l'accord de dominante de la nouvelle tonalité. On note que l'accord de sous-dominante est situé une quinte juste en dessous de la tonique, les accords de dominante et de sous-dominante sont donc symétriques.
En jazz, on utilise également très fréquemment l'accord de la sus-tonique (degré {{Times New Roman|II}}), souvent dans des progressions {{Times New Roman|II}} - {{Times New Roman|V}} (- {{Times New Roman|I}}). Rappelons que l'accord de sus-tonique a la fonction de sous-dominante.
=== Cadences et ''turnaround'' ===
Le terme « cadence » provient de l'italien ''cadenza'' et désigne la « chute », la fin d'un morceau ou d'une phrase musicale.
On distingue deux types de cadences :
* les cadences conclusive, qui créent une sensation de complétude ;
* les cadences suspensives, qui crèent une sensation d'attente.
==== Cadence parfaite ====
[[Fichier:Au clair de le lune cadence parfaite.midi|thumb|''Au clair de la lune'', harmonisé avec une cadence parfaite (italienne).]]
[[Fichier:Au clair de le lune mineur cadence parfaite.midi|thumb|''Idem'' mais en mode mineur harmonique.]]
La cadence parfaite est l'enchaînement de l'accord de dominante suivi de l'accord parfait : {{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}}, les deux accord étant à l'état fondamental. Elle donne une impression de stabilité et est donc très souvent utilisée pour conclure un morceau. C'est une cadence conclusive.
On peut aussi utiliser l'accord de septième de dominante, la dissonance introduisant une tension résolue par l'accord parfait : {{Times New Roman|V<sup>7</sup><sub>+</sub> - I<sup>5</sup>}}.
Elle est souvent précédée de l'accord construit sur le IV<sup>e</sup> degré, appelé « accord de préparation », pour former la cadence italienne : {{Times New Roman|IV<sup>5</sup> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}}.
Elle est également souvent précédée du second renversement de l'accord de tonique, qui est alors appelé « appoggiature de la cadence » : {{Times New Roman|I<sup>6</sup><sub>4</sub> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}} (on remarque que les accords {{Times New Roman|I}}<sup>6</sup><sub>4</sub> et {{Times New Roman|V}}<sup>5</sup> ont la basse en commun, et que l'on peut passer de l'un à l'autre par un mouvement conjoint sur les autres notes).
{{clear}}
==== Demi-cadence ====
[[Fichier:Au clair de le lune demi cadence.midi|thumb|''Au clair de la lune'', harmonisé avec une demi-cadence.]]
Une demi-cadence est une phrase ou un morceau se concluant sur l'accord construit sur le cinquième degré. Il provoque une sensation d'attente, de suspens. Il s'agit en général d'une succession {{Times New Roman|II - V}} ou {{Times New Roman|IV - V}}. C'est une cadence suspensive. On uilise rarement un accord de septième de dominante.
{{clear}}
==== Cadence rompue ou évitée ====
La cadence rompue, ou cadence évitée, est succession d'un accord de dominante et d'un accord de sus-dominante, {{Times New Roman|V}} - {{Times New Roman|VI}}. C'est une cadence suspensive.
==== Cadence imparfaite ====
Une cadence imparfaite est une cadence {{Times New Roman|V - I}}, comme la cadence parfaite, mais dont au moins un des deux accords est dans un état renversé.
==== Cadence plagale ====
La cadence plagale — du grec ''plagios'', oblique, en biais — est la succession de l'accord construit sur le quatrième degré, suivi de l'accord parfait : {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}. Elle peut être utilisée après une cadence parfaite ({{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}} - {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}). Elle donne un caractère solennel, voire religieux — elle est parfois appelée « cadence amen » —, elle a un côté antique qui rappelle la musique modale et médiévale<ref>{{lien web |url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/la-cadence-amen-ou-comment-se-dire-adieu-7191921 |titre=La cadence « Amen » ou comment se dire adieu |auteur=Max Dozolme (MAXXI Classique) |site=France Musique |date=2025-04-25 |consulté le=2025-04-25}}.</ref>.
C'est une cadence conclusive.
==== {{lang|en|Turnaround}} ====
[[Fichier:Au clair de le lune turnaround.midi|thumb|Au clair de la lune, harmonisé en style jazz : accords de 7{{e}}, anatole suivie d'un ''{{lang|en|turnaround}}'' ii-V-I.]]
Le terme ''{{lang|en|turnaround}}'' signifie revirement, retournement. C'est une succession d'accords que fait la transition entre deux parties, en créant une tension-résolution. Le ''{{lang|en|turnaround}}'' le plus courant est la succession {{Times New Roman|II - V - I}}.
On utilise également fréquemment l'anatole : {{Times New Roman|I - VI - II - V}}.
{| class="wikitable"
|+ Progressions typiques d'accords dans une tonalité majeure
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Cadence<br />parfaite<br /> {{Times New Roman|V - I}}
! scope="col" | Cadence<br />italienne<br />{{Times New Roman|IV - V - I}}
! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou IV - V}}
! scope="col" | Cadence<br />plagale<br />{{Times New Roman|IV - I}}
! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}}
! scope="col" | Anatole<br />{{Times New Roman|I - vi - ii - V}}
|-
|''Do'' majeur || || G - C || F - G - C || Dm - G ou F - G || F - C || Dm - G - C || C - Am - Dm - G
|-
|''Sol'' majeur || ''fa''♯ || D - G || C - D - G || Am - D ou C - D || C - G || Am - D - G || G - Em - Am - D
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || A - D || G - A - D || Em - A ou G - A || G - D || Em - A - D || D - Bm - Em - A
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || E - A || D - E - A || Bm - E ou D - E || D - A || Bm - E - A || A - F♯m - B - E
|-
| ''Fa'' majeur || ''si''♭ || C - F || B♭ - C - F || Gm - C ou B♭ - C || B♭ - F || Gm - C - F || F - Dm - Gm - C
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || F - B♭ || E♭ - F - B♭ || Cm - F ou E♭ - F || E♭ - B♭ || Cm - F - B♭ || B♭ - Gm - Cm - F
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || B♭ - E♭ || A♭ - B♭ - E♭ || Fm - B♭ ou A♭ - B♭ || A♭ - E♭ || Fm - B♭ - E♭ || Gm - Cm - Fm - B♭
|}
{| class="wikitable"
|+ Progressions typiques d'accords dans une tonalité mineure
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Cadence<br />parfaite<br />{{Times New Roman|V - i}}
! scope="col" | Cadence<br />italienne<br />{{Times New Roman|iv - V - i}}
! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou iv - V}}
! scope="col" | Cadence<br />plagale<br />{{Times New Roman|iv - i}}
! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}}
! scope="col" | Anatole<br />{{Times New Roman|i - VI - ii - V}}
|-
| ''La'' mineur<br />harmonique || || E - Am || Dm - E - Am || B° - E ou Dm - E || Dm - Am || B° - E - Am || Am - F - B° - E
|-
| ''Mi'' mineur<br />harmonique || ''fa''♯ || B - Em || Am - B - Em || F♯° - B ou Am - B || Am - Em || F♯° - B - Em || Em - C - F♯° - B
|-
| ''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || F♯ - Bm || Em - F♯ - Bm || C♯° - F♯ ou Em - F♯ || Em - Bm || C♯° - F♯ - Bm || Bm - G - C♯° - F♯
|-
| ''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || C♯ - F♯m || Bm - C♯ - F♯m || G♯° - C♯ ou Bm - C♯ || Bm - F♯m || G♯° - C♯ - F♯m || A+ - D - G♯° - C♯
|-
| ''Ré'' mineur<br />harmonique || ''si''♭ || A - Dm || Gm - A - Dm || E° - A ou Gm - A || Gm - Dm || E° - A - Dm || Dm - B♭ - E° - A
|-
| ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || D - Gm || Cm - D - Gm || A° - D ou Cm - D || Cm - Gm|| A° - D - Gm || Gm - E♭ - A° - D
|-
| ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || G - Cm || Fm - G - Cm || D° - G ou Fm - G || Fm - Dm || D° - G - Cm || Cm - A♭ - D° - G
|}
==== Exemple : ''La Mer'' ====
: {{lien web
| url = https://www.youtube.com/watch?v=PXQh9jTwwoA
| titre = Charles Trenet - La mer (Officiel) [Live Version]
| site = YouTube
| auteur = Charles Trenet
| consulté le = 2020-12-24
}}
Le début de ''La Mer'' (Charles Trenet, 1946) est en ''do'' majeur et est harmonisé par l'anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (C - Am - Dm - G<sup>7</sup>) sur deux mesures, jouée deux fois ({{Times New Roman|1=<nowiki>|I-vi|ii-V</nowiki><sup>7</sup><nowiki>|</nowiki>}} × 2). Viennent des variations avec les progressions {{Times New Roman|I-III-vi-V<sup>7</sup>}} (C - E - Am - G<sup>7</sup>) puis la « progression ’50s » (voir plus bas) {{Times New Roman|I-vi-IV-VI<sup>7</sup>}} (C - Am - F - A<sup>7</sup>, on remarque que {{Times New Roman|IV}}/F est le relatif majeur du {{Times New Roman|ii}}/Dm de l'anatole), jouées chacune une fois sur deux mesure ; puis cette première partie se conclut par une demie cadence {{Times New Roman|ii-V<sup>7</sup>}} sur une mesure puis une dernière anatole sur trois mesures ({{Times New Roman|1=<nowiki>|I-vi|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}}). Cela constitue une première partie « A » sur douze mesures qui se termine par une demi-cadence ({{Times New Roman|ii-V<sup>7</sup>}}) qui appelle une suite. Cette partie A est jouée une deuxième fois mais la fin est modifiée pour la transition : les deux dernières mesures {{Times New Roman|<nowiki>|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}} deviennent {{Times New Roman|<nowiki>|ii-V</nowiki><sup>7</sup><nowiki>|I|</nowiki>}} (|Dm-G7|C|), cette partie « A’ » se conclut donc par une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}).
Le morceau passe ensuite en tonalité de ''mi'' majeur, donc une tierce au dessus de ''do'' majeur, sur six mesures. Cette partie utilise une progression ’50s {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (E - C♯m - A - B<sup>7</sup>), qui est rappelons-le une variation de l'anatole, l'accord {{Times New Roman|ii}} (Fm) étant remplacé par son relatif majeur {{Times New Roman|IV}} (A). Cette anatole modifiée est jouée deux fois puis la partie en ''mi'' majeur se conclut par l'accord parfait {{Times New Roman|I}} joué sur deux mesures (|E|E|), on a donc, avec la mesure précédente, avec une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}).
Suivent ensuite six mesures en ''sol'' majeur, donc à nouveau une tierce au dessus de ''mi'' majeur. Elle comporte une progression {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (G - Em - C - D<sup>7</sup>), donc anatole avec substitution du {{Times New Roman|ii}}/Am par son relatif majeur {{Times New Roman|VI}}/C (progression ’50s), puis une anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (G - Em - Am - D<sup>7</sup>) et deux mesure sur la tonique {{Times New Roman|I-I<sup>7</sup>}} (G - G<sup>7</sup>), formant à nouveau une cadence parfaite. La fin sur un accord de septième, dissonant, appelle une suite.
Cette partie « B » de douze mesures comporte donc deux parties similaires « B1 » et « B2 » qui forment une marche harmonique (montée d'une tierce).
Le morceau se conclut par une reprise de la partie « A’ » et se termine donc par une cadence parfaite.
Nous avons une structure A-A’-B-A’ sur 48 mesures, proche la forme AABA étudiée plus loin.
Donc ''La Mer'' est un morceau structuré autour de l'anatole avec des variations (progression ’50s, substitution du {{Times New Roman|ii}} par son relatif majeur {{Times New Roman|IV}}) et comportant une marche harmonique dans sa troisième partie. Les parties se concluent par des ''{{lang|en|turnarounds}}'' sous la forme d'une cadence parfaite ou, pour la partie A, par une demi-cadence.
{| border="1" rules="rows" frame="hsides"
|+ Structure de ''La Mer''
|- align="center"
|
| colspan="12" | ''do'' majeur
|
|- align="center"
! scope="row" rowspan=2 | A
| colspan="2" | anatole
| colspan="2" | //
| colspan="2" | variation
| colspan="2" | ’50s
| ½ c.
| colspan="3" | anatole
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii}} || <nowiki>|</nowiki> {{Times New Roman|V<sup>7</sup>}} || <nowiki>|</nowiki>
|- align="center"
! scope="row" rowspan="2" | A’
| colspan="2" | anatole
| colspan="2" | //
| colspan="2" | variation
| colspan="2" | ’50s
| ½ c.
| colspan="2" | anatole
| c.p.
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki>
|- align="center"
|
| colspan="6" | B1 : ''mi'' majeur
| colspan="6" background="lightgray" | B2 : ''sol'' majeur
|
|- align="center"
! scope="row" rowspan="2" | B
| colspan="2" | ’50s
| colspan="2" | //
|colspan="2" | c.p.
| colspan="2" | ’50s
| colspan="2" | //
|colspan="2" | c.p.
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I<sup>7</sup>}} || <nowiki>|</nowiki>
|-
! scope="row" | A’
| colspan="12" |
|
|}
=== Progression blues ===
La musique blues est apparue dans les années 1860. Elle est en général bâtie sur une grille d'accords ''({{lang|en|changes}})'' immuable de douze mesures ''({{lang|en|twelve-bar blues}})''. C'est sur cet accompagnement qui se répète que s'ajoute la mélodie — chant et solo. Cette structure est typique du blues et se retrouve dans ses dérivés comme le rock 'n' roll.
Le rythme est toujours un rythme ternaire syncopé ''({{lang|en|shuffle, swing, groove}}, ''notes inégales'')'' : la mesure est à quatre temps, mais la noire est divisée en noire-croche en triolet, ou encore triolet de croche en appuyant la première et la troisième.
La mélodie se construit en général sur une gamme blues de six degrés (gamme pentatonique mineure avec une quarte augmentée), mais bien que la gamme soit mineure, l'harmonie est construite sur la gamme majeure homonyme : un blues en ''fa'' a une mélodie sur la gamme de ''fa'' mineur, mais une harmonie sur la gamme de ''fa'' majeur. La grille d'accord comporte les accords construits sur les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la gamme majeure homonyme. Les accords sont souvent des accords de septième (donc avec une tierce majeure et une septième mineure), il ne s'agit donc pas d'une harmonisation de gamme diatonique (puisque la septième est majeure sur l'accord de tonique).
Par exemple, pour un blues en ''do'' :
* accord parfait de do majeur, C ({{Times New Roman|I}}<sup>er</sup> degré) ;
* accord parfait de fa majeur, F ({{Times New Roman|IV}}<sup>e</sup> degré) ;
* accord parfait de sol majeur, G ({{Times New Roman|V}}<sup>e</sup> degré).
Il existe quelques morceaux harmonisés avec des accords mineurs, comme par exemple ''As the Years Go Passing By'' d'Albert King (Duje Records, 1959).
La progression blues est organisée en trois blocs de quatre mesures ayant les fonctions suivantes (voir ci-dessus ''[[#Harmonie fonctionnelle|Harmonie fonctionnelle]]'') :
* quatre mesures toniques ;
* quatre mesures sous-dominantes ;
* quatre mesures dominantes.
La forme la plus simple, que Jeff Gardner appelle « forme A », est la suivante :
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, forme A
|-
! scope="row" | Tonique
| width="50px" | I
| width="50px" | I
| width="50px" | I
| width="50px" | I
|-
! scope="row" | Sous-domminante
| width="50px" | IV
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
! scope="row" | Dominante
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | V
|}
La progression {{Times New Roman|I-V}} des deux dernières mesures forment le ''{{lang|en|turnaround}}'', la demie cadence qui lance le cycle suivant. Nous présentons ci-dessous un exemple typique de ligne de basse ''({{lang|en|walking bass}})'' pour le ''{{lang|en|turnaround}}'' d'un blues en ''la'' :
[[Fichier:Turnaround classique blues en la.svg|Exemple typique de ligne de basse pour un ''turnaround'' de blues en ''la''.]]
[[Fichier:Blues mi harmonie elementaire.midi|thumb|Blues en ''mi'', harmonisé de manière élémentaire avec une ''{{lang|en|walking bass}}''.]]
Vous pouvez écouter ci-contre une harmonisation typique d'un blues en ''mi''. Les accords sont exécutés par une basse marchante ''({{lang|en|walking bass}})'', qui joue une arpège sur la triade avec l'ajout d'une sixte majeure et d'une septième mineure, et par une guitare qui joue un accord de puissance ''({{lang|en|power chord}})'', qui n'est composé que de la fondamentale et de la quinte juste, avec une sixte en appoggiature.
La forme B s'obtient en changeant la deuxième mesure : on joue un degré {{Times New Roman|IV}} au lieu d'un degré {{Times New Roman|I}}. La progression {{Times New Roman|I-IV}} sur les deux premières mesures est appelé ''{{lang|en|quick change}}''.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, forme B
|-
| width="50px" | I
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
| width="50px" | IV
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | V
|}
Par exemple, ''Sweet Home Chicago'' (Robert Johnson, 1936) est un blues en ''fa'' ; sa grille d'accords, aux variations près, suit une forme B :
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression de ''Sweet Home Chicago''
|-
| width="50px" | F
| width="50px" | B♭
| width="50px" | F
| width="50px" | F
|-
| width="50px" | B♭
| width="50px" | B♭
| width="50px" | F
| width="50px" | F
|-
| width="50px" | C7
| width="50px" | B♭7
| width="50px" | F7
| width="50px" | C7
|}
: Écouter {{lien web
| url =https://www.youtube.com/watch?v=dkftesK2dck
| titre = Robert Johnson "Sweet Home Chicago"
| auteur = Michal Angel
| site = YouTube
| date = 2007-12-09 | consulté le = 2020-12-17
}}
Les formes C et D s'obtiennent à partir des formes A et B en changeant le dernier accord par un accord sur le degré {{Times New Roman|I}}, ce qui forme une cadence plagale.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, formes C et D
|-
| colspan="4" | …
|-
| colspan="4" | …
|-
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|}
L'harmonie peut être enrichie, notamment en jazz. Voici par exemple une grille du blues souvent utilisés en bebop.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Exemple de progression de blues bebop sur une base de forme B
|-
| width="60px" | I<sup>7</sup>
| width="60px" | IV<sup>7</sup>
| width="60px" | I<sup>7</sup>
| width="60px" | V–<sup>7</sup> <nowiki>|</nowiki> I<sup>7</sup>
|-
| width="60px" | IV<sup>7</sup>
| width="60px" | IV<sup>7</sup>
| width="60px" | I<sup>7</sup>
| width="60px" | VI<sup>7 ♯9 ♭13</sup>
|-
| width="60px" | II–<sup>7</sup>
| width="60px" | V<sup>7</sup>
| width="60px" | V<sup>7</sup> <nowiki>|</nowiki> IV<sup>7</sup>
| width="60px" | II–<sup>7</sup> <nowiki>|</nowiki> V<sup>7</sup>
|}
On peut aussi trouver des blues sur huit mesures, sur seize mesures comme ''Watermelon Man'' de Herbie Hancock (album ''Takin' Off'', Blue Note, 1962) ou ''Let's Dance'' de Jim Lee (interprété par Chris Montez, Monogram, 1962)
* {{lien web
|url= https://www.dailymotion.com/video/x5iduwo
|titre=Herbie Hancock - Watermelon Man (1962)
|auteur=theUnforgettablesTv
|site=Dailymotion
|date=2003 |consulté le=2021-02-09
}}
* {{lien web
|url=https://www.youtube.com/watch?v=6JXshurYONc
|titre=Let's Dance
|auteur=Chris Montez
|site=YouTube
|date=2016-08-06 |consulté le=2021-02-09
}}
À l'inverse, certains blues peuvent avoir une structure plus simple que les douze mesure ; par exemple ''Hoochie Coochie Man'' de Willie Dixon (interprété par Muddy Waters sous le titre ''Mannish Boy'', Chicago Blues, 1954) est construit sur un seul accord répété tout le long de la chanson.
* {{lien web
|url=https://www.dailymotion.com/video/x5iduwo
|titre=Muddy Waters - Hoochie Coochie Man
|auteur=Muddy Waters
|site=Dailymotion
|date=2012 | consulté le=2021-02-09
}}
=== Cadence andalouse ===
La cadence andalouse est une progression de quatre accords, descendant par mouvement conjoint :
* en mode de ''mi'' (mode phrygien) : {{Times New Roman|IV}} - {{Times New Roman|III}} - {{Times New Roman|II}} - {{Times New Roman|I}} ;<br />par exemple en ''mi'' phrygien : Am - G - F - E ; en ''do'' phrygien : Fm - E♭ - D♭ - C ;<br />on notera que le degré {{Times New Roman|III}} est diésé dans l'accord final (ou bécarre s'il est bémol dans la tonalité) ;
* en mode mineur : {{Times New Roman|I}} - {{Times New Roman|VII}} - {{Times New Roman|VI}} - {{Times New Roman|V}} ;<br />par exemple en ''la'' mineur : Am - G - F - E ; en ''do'' mineur : Cm - B♭ - A♭ - m ;<br />comme précédemment, on notera que le degré {{Times New Roman|VII}} est diésé dans l'accord final.
=== Progressions selon le cercle des quintes ===
[[Fichier:Cercle quintes degres tonalite majeure.svg|vignette|Cercle des quinte justes (parcouru dans le sens des aiguilles d'une montre) des degrés d'une tonalité majeure.]]
La progression {{Times New Roman|V-I}} est la cadence parfaite, mais on peut aussi l'employer au milieu d'un morceau. Cette progression étant courte, sa répétition crée de la lassitude ; on peut la compléter par d'autres accords séparés d'une quinte juste, en suivant le « cercle des quintes » : {{Times New Roman|I-V-IX}}, la neuvième étant enharmonique de la seconde, on obtient {{Times New Roman|I-V-II}}.
On peut continuer de décrire le cercle des quintes : {{Times New Roman|I-V-II-VI}}, on obtient l'anatole dans le désordre ; on peut à l'inverse étendre les quintes vers la gauche, {{Times New Roman|IV-I-V-II-VI}}.
En musique populaire, on trouve fréquemment une progression fondée sur les accord {{Times New Roman|I}}, {{Times New Roman|IV}}, {{Times New Roman|V}} et {{Times New Roman|VI}}, popularisée dans les années 1950. La « progression années 1950 », « progression ''{{lang|en|fifties ('50)}}'' » ''({{lang|en|'50s progression}})'' est dans l'ordre {{Times New Roman|I-VI-IV-V}}. On trouve aussi cette progression en musique classique. Si la tonalité est majeure, la triade sur la sus-dominante est mineure, les autres sont majeures, on notera donc souvent {{Times New Roman|I-vi-IV-V}}. On peut avoir des permutations circulaires (le dernier accord venant au début, ou vice-versa) : {{Times New Roman|vi-IV-V-I}}, {{Times New Roman|IV-V-I-vi}} et {{Times New Roman|V-I-vi-IV}}.
{| class="wikitable"
|+ Accords selon la tonalité
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" style="font-family:Times New Roman" | I
! scope="col" style="font-family:Times New Roman" | IV
! scope="col" style="font-family:Times New Roman" | V
! scope="col" style="font-family:Times New Roman" | vi
|-
|''Do'' majeur || || C || F || G || Am
|-
|''Sol'' majeur || ''fa''♯ || G || C || D || Em
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || D || G || A || Bm
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A || D || E || F♯m
|-
| ''Fa'' majeur || ''si''♭ || F || B♭ || C || Dm
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭ || E♭ || F || Gm
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭ || A♭ || B♭ || Cm
|}
Par exemple, en tonalité de ''do'' majeur, la progression {{Times New Roman|I-vi-IV-V}} sera C-Am-F-G.
Il existe d'autres progressions utilisant ces accords mais dans un autre ordre, typiquement {{Times New Roman|I–IV–vi–V}} ou une de ses permutations circulaires : {{Times New Roman|IV–vi–V-I}}, {{Times New Roman|vi–V-I-IV}} ou {{Times New Roman|V-I-IV-vi}}. Ou dans un autre ordre.
PV Nova l'illustre dans plusieurs de ses « expériences » dans la version {{Times New Roman|vi-V-IV-I}}, soit Am-G-F-C, ou encore {{Times New Roman|vi-IV-I-V}}, soit Am-F-C-G :
: {{lien web
| url = https://www.youtube.com/watch?v=w08LeZGbXq4
| titre = Expérience n<sup>o</sup> 6 — La Happy Pop
| auteur = PV Nova
| site = YouTube
| date = 2011-08-20 | consulté le = 2020-12-13
}}
et cela devient un gag récurrent avec son « chapeau des accords magiques qu'on nous ressort à toutes les sauces »
: {{lien web
| url = https://www.youtube.com/watch?v=VMY_vc4nZAU
| titre = Expérience n<sup>o</sup> 14 — La Soupe dou Brasil
| auteur = PV Nova
| site = YouTube
| date = 2012-10-03 | consulté le = 2020-12-17
}}
Cette récurrence est également parodiée par le groupe The Axis of Awesome avec ses « chansons à quatre accords » ''({{lang|en|four-chords song}})'', dans une sketch où ils mêlent 47 chansons en utilisant l'ordre {{Times New Roman|I-V-vi-IV}} :
: {{lien web
| url = https://www.youtube.com/watch?v=oOlDewpCfZQ
| titre = 4 Chords | Music Videos | The Axis Of Awesome
| auteur = The Axis of Awesome
| site = YouTube
| date = 2011-07-20 | consulté le = 2020-12-17
}}
{{boîte déroulante/début|titre=Chansons mêlées dans le sketch}}
# Journey : ''Don't Stop Believing'' ;
# James Blunt : ''You're Beautiful'' ;
# Black Eyed Peas : ''Where Is the Love'' ;
# Alphaville : ''Forever Young'' ;
# Jason Mraz : ''I'm Yours'' ;
# Train : ''Hey Soul Sister'' ;
# The Calling : ''Wherever You Will Go'' ;
# Elton John : ''Can You Feel The Love Tonight'' (''Le Roi lion'') ;
# Akon : ''Don't Matter'' ;
# John Denver : ''Take Me Home, Country Roads'' ;
# Lady Gaga : ''Paparazzi'' ;
# U2 : ''With Or Without You'' ;
# The Last Goodnight : ''Pictures of You'' ;
# Maroon Five : ''She Will Be Loved'' ;
# The Beatles : ''Let It Be'' ;
# Bob Marley : ''No Woman No Cry'' ;
# Marcy Playground : ''Sex and Candy'' ;
# Men At Work : ''Land Down Under'' ;
# thème de ''America's Funniest Home Videos'' (équivalent des émissions ''Vidéo Gag'' et ''Drôle de vidéo'') ;
# Jack Johnson : ''Taylor'' ;
# Spice Girls : ''Two Become One'' ;
# A Ha : ''Take On Me'' ;
# Green Day : ''When I Come Around'' ;
# Eagle Eye Cherry : ''Save Tonight'' ;
# Toto : ''Africa'' ;
# Beyonce : ''If I Were A Boy'' ;
# Kelly Clarkson : ''Behind These Hazel Eyes'' ;
# Jason DeRulo : ''In My Head'' ;
# The Smashing Pumpkins : ''Bullet With Butterfly Wings'' ;
# Joan Osborne : ''One Of Us'' ;
# Avril Lavigne : ''Complicated'' ;
# The Offspring : ''Self Esteem'' ;
# The Offspring : ''You're Gonna Go Far Kid'' ;
# Akon : ''Beautiful'' ;
# Timberland featuring OneRepublic : ''Apologize'' ;
# Eminem featuring Rihanna : ''Love the Way You Lie'' ;
# Bon Jovi : ''It's My Life'' ;
# Lady Gaga : ''Pokerface'' ;
# Aqua : ''Barbie Girl'' ;
# Red Hot Chili Peppers : ''Otherside'' ;
# The Gregory Brothers : ''Double Rainbow'' ;
# MGMT : ''Kids'' ;
# Andrea Bocelli : ''Time To Say Goodbye'' ;
# Robert Burns : ''Auld Lang Syne'' ;
# Five for fighting : ''Superman'' ;
# The Axis of Awesome : ''Birdplane'' ;
# Missy Higgins : ''Scar''.
{{boîte déroulante/fin}}
Vous pouvez par exemple jouer les accords C-G-Am-F ({{Times New Roman|I-V-vi-IV}}) et chanter dessus ''{{lang|en|Let It Be}}'' (Paul McCartney, The Beattles, 1970) ou ''Libérée, délivrée'' (Robert Lopez, ''La Reine des neiges'', 2013).
La progression {{Times New Roman|I-V-vi-IV}} est considérée comme « optimiste » tandis que sa variante {{Times New Roman|iv-IV-I-V}} est considérée comme « pessimiste ».
On peut voir la progression {{Times New Roman|I-vi-IV-V}} comme une variante de l'anatole {{Times New Roman|I-vi-ii-V}}, obtenue en remplaçant l'accord de sustonique {{Times New Roman|ii}} par l'accord de sous-dominante {{Times New Roman|IV}} (son relatif majeur, et degré ayant la même fonction).
==== Exemples de progression selon le cercle des quintes en musique classique ====
[[Fichier:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.midi|vignette|Dietrich Buxtehude, Psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]]
Cette progression selon la cercle des quintes, sous la forme {{Times New Roman|I-vi-IV-V}}, apparaît déjà au {{pc|xvii}}<sup>e</sup> siècle dans le psaume 42 ''Quem ad modum desiderat cervis'' (BuxVW92) de Dietrich Buxtehude (1637-1707). Le morceau est en ''fa'' majeur, la progression d'accords est donc F-Dm-B♭-C.
: {{lien web
| url = https://www.youtube.com/watch?v=8FmV9l1RqSg
| titre = D. Buxtehude - Quemadmodum desiderat cervus, BuxWV 92
| auteur = Longobardo
| site = YouTube
| date = 2013-04-06 | consulté la = 2021-01-01
}}
[[File:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.svg|vignette|450x450px|center|Dietrich Buxtehude, psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]]
{{clear}}
[[Fichier:JSBach BWV140 cantate 4 mesures.midi|vignette|J.-S. Bach, cantate BWV140, quatre premières mesures.]]
On la trouve également dans l'ouverture de la cantate ''{{lang|de|Wachet auf, ruft uns die Stimme}}'' de Jean-Sébastien Bach (BWV140, 1731). Le morceau est en ''mi''♭ majeur, la progression d'accords est donc E♭-Cm-A♭<sup>6</sup>-B♭.
[[Fichier:JSBach BWV140 cantate 4 mesures.svg|vignette|center|J.-S. Bach, cantate BWV140, quatre premières mesures.|alt=|517x517px]]
{{clear}}
[[Fichier:Mozart K310 Sonate8 mesures 45 a 49.midi|vignette|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]]
La même progression est utilisée par Mozart, par exemple dans le premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778), la progression d'accords est C-Am-F-G qui correspond à la progression {{Times New Roman|III-i-VI-VII}} de ''la'' mineur, mais à la progression {{Times New Roman|I-vi-IV-V}} de la gamme relative, ''do'' majeur .
[[Fichier:Mozart K310 Sonate8 mesures 45 a 49.svg|vignette|center|500px|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]]
=== Substitution tritonique ===
Un des accords les plus utilisés est donc l'accord de septième de dominante, {{Times New Roman|V<sup>7</sup><sub>+</sub>}} qui contient les degrés {{Times New Roman|V}}, {{Times New Roman|VII}}, {{Times New Roman|II}} ({{Times New Roman|IX}}) et {{Times New Roman|IV}}({{Times New Roman|XI}}) ; par exemple, en tonalité de ''do'' majeur, l'accord de ''sol'' septième (G<sup>7</sup>) contient les notes ''sol''-''si''-''ré''-''fa''. Si l'on prend l'accord dont la fondamentale est trois tons (triton) au-dessus ou en dessous — l'octave contenant six tons, on arrive sur la même note —, {{Times New Roman|♭II<sup>7</sup>}}, ici ''ré''♭ septième (D♭<sup>7</sup>), celui-ci contient les notes ''ré''♭-''fa''-''la''♭-''do''♭, cette dernière note étant l'enharmonique de ''si''. Les deux accords G<sup>7</sup> et D♭<sup>7</sup> ont donc deux notes en commun : le ''fa'' et le ''si''/''do''♭.
Il est donc fréquent en jazz de substituer l'accord {{Times New Roman|V<sup>7</sup><sub>+</sub>}} par l'accord {{Times New Roman|♭II<sup>7</sup>}}. Par exemple, la progression {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|V<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}} devient {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|♭II<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}}. C'est un procédé courant de réharmonisation (le fait de remplacer un accord par un autre dans un morceau existant).
Les six substitutions possibles sont donc : C<sup>7</sup>↔F♯<sup>7</sup> - D♭<sup>7</sup>↔G<sup>7</sup> - D<sup>7</sup>↔A♭<sup>7</sup> - E♭<sup>7</sup>↔A<sup>7</sup> - E<sup>7</sup>↔B♭<sup>7</sup> - F<sup>7</sup>↔B<sup>7</sup>.
[[Fichier:Übermäsiger Terzquartakkord.jpg|vignette|Exemple de cadence parfaite en ''do'' majeur avec substitution tritonique (sixte française).]]
Dans l'accord D♭<sup>7</sup>, si l'on remplace le ''do''♭ par son ''si'' enharmonique, on obtient un accord de sixte augmentée : ''ré''♭-''fa''-''la''♭-''si''. Cet accord est utilisé en musique classique depuis la Renaissance ; on distingue en fait trois accords de sixte augmentée :
* sixte française ''ré''♭-''fa''-''sol''-''si'' ;
* sixte allemande : ''ré''♭-''fa''-''la''♭-''si'' ;
* sixte italienne : ''ré''♭-''fa''-''si''.
Par exemple, le ''Quintuor en ''ut'' majeur'' de Franz Schubert (1828) se termine par une cadence parfaite dont l'accord de dominante est remplacé par une sixte française ''ré''♭-''fa''-''si''-''sol''-''si'' (''ré''♭ aux violoncelles, ''fa'' à l'alto, ''si''-''sol'' aux seconds violons et ''si'' au premier violon).
[[Fichier:Schubert C major Quintet ending.wav|vignette|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]]
[[Fichier:Schubert C major Quintet ending.png|vignette|center|upright=2.5|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]]
=== Autres accords de substitution ===
Substituer un accord consiste à utiliser un accord provenant d'une tonalité étrangère à la tonalité en cours. À la différence d'une modulation, la substitution est très courte et ne donne pas l'impression de changer de tonalité ; on a juste un sentiment « étrange » passager. Un court passage dans une autre tonalité est également appelée « emprunt ».
Nous avons déjà vu plusieurs méthodes de substitution :
* utilisation d'une note étrangère : une note étrangère — note de passage, appoggiature, anticipation, retard… — crée momentanément un accord hors tonalité ; en musique classique, ceci n'est pas considéré comme un accord en propre, mais en jazz, on parle « d'accord de passage » et « d'accord suspendu » ;
* utilisation d'une dominante secondaire : l'accord de dominante secondaire est hors tonalité ; le but ici est de faire une cadence parfaite, mais sur un autre degré que la tonique de la tonalité en cours ;
* la substitution tritonique, vue ci-dessus, pour remplacer un accord de septième de dominante.
Une dernière méthode consiste à remplacer un accord par un accord d'une gamme de même tonique, mais d'un autre mode ; on « emprunte » ''({{lang|en|borrow}})'' l'accord d'un autre mode. Par exemple, substituer un accord de la tonalité de ''do'' majeur par un accord de la tonalité de ''do'' mineur ou de ''do'' mode de ''mi'' (phrygien).
Donc en ''do'' majeur, on peut remplacer un accord de ''ré'' mineur septième (D<sub>m</sub><sup>7</sup>) par un accord de ''ré'' demi-diminué (D<sup>⌀</sup>, D<sub>m</sub><sup>7♭5</sup>) qui est un accord appartenant à la donalité de ''la'' mineur harmonique.
=== Forme AABA ===
La forme AABA est composée de deux progressions de huit mesures, notées A et B ; cela représente trente-deux mesures au total, on parle donc souvent en anglais de la ''{{lang|en|32-bars form}}''. C'est une forme que l'on retrouve dans de nombreuses chanson de comédies musicales de Broadway comme ''Have You Met Miss Jones'' (''{{lang|en|I'd Rather Be Right}}'', 1937), ''{{lang|en|Over the Rainbow}}'' (''Le Magicien d'Oz'', Harold Harlen, 1939), ''{{lang|en|All the Things You Are}}'' (''{{lang|en|Very Warm for may}}'', 1939).
Par exemple, la version de ''{{lang|en|Over the Rainbow}}'' chantée par Judy Garland est en ''la''♭ majeur et la progression d'accords est globalement :
* A (couplet) : A♭-Fm | Cm-A♭ | D♭ | Cm-A♭ | D♭ | D♭-F | B♭-E♭ | A♭
* B (pont) : A♭ | B♭m | Cm | D♭ | A♭ | B♭-G | Cm-G | B♭m-E♭
soit en degrés :
* A : {{Times New Roman|<nowiki>I-vi | iii-I | IV | iii-IV | IV | IV-vi | II-V | I</nowiki>}}
* B : {{Times New Roman|<nowiki>I | ii | iii | IV | I | II-VII | iii-VII | ii-V</nowiki>}}
Par rapport aux paroles de la chanson, on a
* A : couplet 1 ''« {{lang|en|Somewhere […] lullaby}} »'' ;
* A : couplet 2 ''« {{lang|en|Somewhere […] really do come true}} »'' ;
* B : pont ''« {{lang|en|Someday […] you'll find me}} »'' ;
* A : couplet 3 ''« {{lang|en|Somewhere […] oh why can't I?}} »'' ;
: {{lien web
| url = https://www.youtube.com/watch?v=1HRa4X07jdE
| titre = Judy Garland - Over The Rainbow (Subtitles)
| site = YouTube
| auteur = Overtherainbow
| consulté le = 2020-12-17
}}
Une mise en œuvre de la forme AABA couramment utilisée en jazz est la forme anatole (à le pas confondre avec la succession d'accords du même nom), en anglais ''{{lang|en|rythm changes}}'' car elle s'inspire du morceau ''{{lang|en|I Got the Rythm}}'' de George Gerschwin (''Girl Crazy'', 1930) :
* A : {{Times New Roman|I–vi–ii–V}} (succession d'accords « anatole ») ;
* B : {{Times New Roman|III<sup>7</sup>–VI<sup>7</sup>–II<sup>7</sup>–V<sup>7</sup>}} (les fondamentales forment une succession de quartes, donc parcourent le « cercle des quintes » à l'envers).
Par exemple, ''I Got the Rythm'' étant en ''ré''♭ majeur, la forme est :
* A : D♭ - B♭m - E♭m - A♭
* B : F7 - B♭7 - E♭7 - A♭7
=== Exemples ===
==== Début du Largo de la symphonie du Nouveau Monde ====
[[File:Largo nouveau monde 5 1res mesures.svg|vignette|Partition avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]]
[[File:Largo nouveau monde 5 1res mesures.midi|vignette|Fichier son avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]]
Nous avons reproduit ci-contre les cinq premières mesure du deuxième mouvement Largo de la symphonie « Du Nouveau Monde » (symphonie n<sup>o</sup> 9 d'Antonín Dvořák, 1893). Cliquez sur l'image pour l'agrandir.
Vous pouvez écouter cette partie jouée par un orchestre symphonique :
* {{lien web
|url =https://www.youtube.com/watch?v=y2Nw9r-F_yQ?t=565
|titre = Dvorak Symphony No.9 "From the New World" Karajan 1966
|site=YouTube (Seokjin Yoon)
|consulté le=2020-12-11
}} (à 9 min 25), par le Berliner Philharmoniker, dirigé par Herbert von Karajan (1966) ;
* {{lien web
|url = https://www.youtube.com/watch?v=ASlch7R1Zvo
|titre=Dvořák: Symphony №9, "From The New World" - II - Largo
|site=YouTube (diesillamusicae)
|consulté le=2020-12-11
}} : Wiener Philharmoniker, dirigé par Herbert von Karajan (1985).
{{clear}}
Cette partie fait intervenir onze instruments monodiques (ne jouant qu'une note à la fois) : des vents (trois bois, sept cuivres) et une percussion. Certains de ces instruments sont transpositeurs (les notes sur la partition ne sont pas les notes entendues). Jouées ensemble, ces onze lignes mélodiques forment des accords.
Pour étudier cette partition, nous réécrivons les parties des instruments transpositeurs en ''do'' et les parties en clef d’''ut'' en clef de ''fa''. Nous regroupons les parties en clef de ''fa'' d'un côté et les parties en clef de ''sol'' d'un autre.
{{boîte déroulante|Résultat|contenu=[[File:Largo nouveau monde 5 1res mesures transpositeurs en do.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde, en do.]]}}
Nous pouvons alors tout regrouper sous la forme d'un système de deux portées clef de ''fa'' et clef de ''sol'', comme une partition de piano.
{{boîte déroulante|Résultat|contenu=
[[Fichier:Largo nouveau monde 5 1res mesures accords.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords.]]
}}
[[Fichier:Largo nouveau monde 5 1res mesures accords.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords.]]
{{clear}}
Ensuite, nous ne gardons que la basse et les notes médium. Nous changeons éventuellement certaines notes d'octave afin de n'avoir que des superpositions de tierce ou de quinte (état fondamental des accords, en faisant ressortir les notes manquantes).
{{boîte déroulante|Résultat|contenu=
[[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords simplifiés.]]
}}
[[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords simplifiés.]]
Vous pouvez écouter cette partie jouée par un quintuor de cuivres (trompette, bugle, cor, trombone, tuba), donc avec des accords de cinq notes :
: {{lien web
|url=https://www.youtube.com/watch?v=pWfe60nbvjA
|titre = Largo from The New World Symphony by Dvorak
|site=YouTube (The Chamberlain Brass)
|consulté le=2020-12-11
}} : The American Academy of Arts & Letters in New York City (2017).
Nous allons maintenant chiffrer les accords.
Pour établir la basse chiffrée, il nous faut déterminer le parcours harmonique. Pour le premier accord, les tonalités les plus simples avec un ''sol'' dièse sont ''la'' majeur et ''fa'' dièse mineur ; comme le ''mi'' est bécarre, nous retenons ''la'' majeur, il s'agit donc d'un accord de quinte sur la dominante (les accords de dominante étant très utilisés, cela nous conforte dans notre choix). Puis nous avons un ''si'' bémol, nous pouvons être en ''fa'' majeur ou en ''ré'' mineur ; nous retenons ''fa'' majeur, c'est donc le renversement d'un accord sur le degré {{Times New Roman|II}}.
Dans la deuxième mesure, nous revenons en ''la'' majeur, puis, avec un ''la'' et un ''ré'' bémols, nous sommes en ''la'' bémol majeur ; nous avons donc un accord de neuvième incomplet sur la sensible, ou un accord de onzième incomplet sur la dominante.
Dans la troisième mesure, nous passons en ''ré'' majeur, avec un accord de dominante. Puis, nous arrivons dans la tonalité principale, avec le renversement d'un accord de dominante sans tierce suivi d'un accord de tonique. Nous avons donc une cadence parfaite, conclusion logique d'une phrase.
La progression des accords est donc :
{| class="wikitable"
! scope="row" | Tonalité
| ''la'' M - ''fa'' M || ''la'' M - ''la''♭ M || ''ré'' M - ''ré''♭ M || ''ré''♭ M
|-
! scope="row" | Accords
| {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|II}}<sup>6</sup><sub>4</sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|“V”}}<sup>9</sup><sub><s>5</s></sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|V}}<sup>+4</sup> || {{Times New Roman|I}}<sup>5</sup>
|}
Dans le chiffrage jazz, nous avons donc :
* une triade de ''mi'' majeur, E ;
* une triade de ''sol'' majeur avec un ''ré'' en basse : G/D ;
* à nouveau un E ;
* un accord de ''sol'' neuvième diminué incomplet, avec un ''ré'' bémol en basse : G dim<sup>9</sup>/D♭ ;
* un accord de ''la'' majeur, A ;
* un accord de ''la'' bémol septième avec une ''sol'' bémol à la basse : A♭<sup>7</sup>/G♭ ;
* la partie se conclue par un accord parfait de ''ré''♭ majeur, D♭.
Soit une progression E - G/D | E - G dim<sup>9</sup>/D♭ | A - A♭<sup>7</sup>/G♭ | D♭.
[[Fichier:Largo nouveau monde 5 1res mesures accords chiffres.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde en accords simplifiés.]]
{{clear}}
==== Thème de Smoke on the Water ====
Le morceau ''Smoke on the Water'' du groupe Deep Purple (album ''Machine Head'', 1972) possède un célèbre thème, un riff ''({{lang|en|rythmic figure}})'', joué à la guitare sous forme d'accords de puissance ''({{lang|en|power chords}})'', c'est-à-dire des accords sans tierce. Le morceau est en tonalité de ''sol'' mineur naturel (donc avec un ''fa''♮) avec ajout de la note bleue (''{{lang|en|blue note}}'', quinte diminuée, ''ré''♭), et les accords composant le thème sont G<sup>5</sup>, B♭<sup>5</sup>, C<sup>5</sup> et D♭<sup>5</sup>, ce dernier accord étant l'accord sur la note bleue et pouvant être considéré comme une appoggiature (indiqué entre parenthèse ci-après). On a donc ''a priori'', sur les deux premières mesures, une progression {{Times New Roman|I-III-IV}} puis {{Times New Roman|I-III-(♭V)-IV}}. Durant la majeure partie du thème, la guitare basse tient la note ''sol'' en pédale.
{{note|En jazz, la qualité « <sup>5</sup> » indique que l'on n'a que la quinte (et donc pas la tierce), contrairement à la notation de basse chiffrée.}}
: {{lien web
| url = https://www.dailymotion.com/video/x5ili04
| titre = Deep Purple — Smoke on the Water (Live at Montreux 2006)
| auteur = Deep Purple
| site = Dailymotion
| date = 2016 | consulté le = 2020-12-31
}}
Cependant, cette progression forme une mélodie, on peut donc plus la voir comme un contrepoint, la superposition de deux voies ayant un mouvement conjoint, joué par un seul instrument, la guitare, la voie 2 étant jouée une quarte juste en dessous de la voie 1 (la quarte juste descendante étant le renversement de la quinte juste ascendante) :
* voie 1 (aigu) : | ''sol'' - ''si''♭ - ''do'' | ''sol'' - ''si''♭ - (''ré''♭) - ''do'' | ;
* voie 2 (grave) : | ''ré'' - ''fa'' - ''sol'' | ''ré'' - ''fa'' - (''la''♭) - ''sol'' |.
En se basant sur la basse (''sol'' en pédale), nous pouvons considérer que ces deux mesures sont accompagnées d'un accord de Gm<sup>7</sup> (''sol''-''si''♭-''ré''-''fa''), chaque accord de la mélodie comprenant à chaque fois au moins une note de cet accord à l'exception de l'appogiature.
{| class="wikitable"
|+ Mise en évidence des notes de l'accord Gm<sup>7</sup>
|-
! scope="row" | Accords
| G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup>
|-
! scope="row" | Voie 1
| '''''sol''''' || '''''si''♭''' || ''do''
|-
! scope="row" | Voie 2
| '''''ré''''' || '''''fa''''' || '''''sol'''''
|-
! scope="row" | Basse
| '''''sol''''' || '''''sol''''' || '''''sol'''''
|}
Sur les deux mesures suivantes, la basse varie et suit les accords de la guitare avec un retard sur le dernier accord :
{| class="wikitable"
|+ Voies sur les mesure 3-4 du thème
|-
! scope="row" | Accords
| G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> || B♭<sup>5</sup> || G<sup>5</sup>
|-
! scope="row" | Voie 1
| ''sol'' || ''si''♭ || ''do'' || ''si''♭ || ''sol''
|-
! scope="row" | Voie 2
| ''ré'' || ''fa'' || ''sol'' || ''fa'' || ''ré''
|-
! scope="row" | Basse
| ''sol'' || ''sol'' || ''do'' || ''si''♭ || ''si''♭-''sol''
|}
Le couplet de cette chanson est aussi organisé sur une progression de quatre mesures, la guitare faisant des arpèges sur les accords G<sup>5</sup> (''sol''-''ré''-''sol'') et F<sup>5</sup> (''fa''-''do''-''fa'') :
: | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-F<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> |
soit une progression {{Times New Roman|<nowiki>| I-I | I-I | I-VII | I-I |</nowiki>}}. Nous pouvons aussi harmoniser le riff du thème sur cette progression, avec un accord F (''fa''-''la''-''do'') ; nous pouvons aussi nous rappeler que l'accord sur le degré {{Times New Roman|VII}} est plus volontiers considéré comme un accord de septième de dominante {{Times New Roman|V<sup>7</sup>}}, soit ici un accord Dm<sup>7</sup> (''ré''-''fa''-''la''-''do''). On peut donc considérer la progression harmonique sur le thème :
: | Gm-Gm | Gm-Gm | Gm-F ou Dm<sup>7</sup> | Gm-Gm |.
Cette analyse permet de proposer une harmonisation enrichie du morceau, tout en se rappelant qu'une des forces du morceau initial est justement la simplicité de sa structure, qui fait ressortir la virtuosité des musiciens. Nous pouvons ainsi comparer la version album à la version concert avec orchestre ou à la version latino de Pat Boone. À l'inverse, le groupe Psychostrip, dans une version grunge, a remplacé les accords par une ligne mélodique :
* le thème ne contient plus qu'une seule voie (la guitare ne joue pas des accords de puissance) ;
* dans les mesures 9 et 10, la deuxième guitare joue en contrepoint de type mouvement inverse, qui est en fait la voie 2 jouée en miroir ;
* l'arpège sur le couplet est remplacé par une ligne mélodique en ostinato sur une gamme blues.
{| class="wikitable"
|+ Contrepoint sur les mesures 9 et 10
|-
! scope="row" | Guitare 1
| ''sol'' ↗ ''si''♭ ↗ ''do''
|-
! scope="row" | Guitare 2
| ''sol'' ↘ ''fa'' ↘ ''ré''
|}
* {{lien web
| url = https://www.dailymotion.com/video/x5ik234
| titre = Deep Purple — Smoke on the Water (In Concert with the London Symphony Orchestra, 1999)
| auteur = Deep Purple
| site = Dailymotion
| date = 2016 | consulté le = 2020-12-31
}}
* {{lien web
| url = https://www.youtube.com/watch?v=MtUuNzVROIg
| titre = Pat Boone — Smoke on the Water (In a Metal Mood, No More Mr. Nice Guy, 1997)
| auteur = Orrore a 33 Giri
| site = YouTube
| date = 2019-06-24 | consulté le = 2020-12-31
}}
* {{lien web
| url = https://www.youtube.com/watch?v=n7zLlZ8B0Bk
| titre = Smoke on the Water (Heroes, 1993)
| auteur = Psychostrip
| site = YouTube
| date = 2018-06-20 | consulté le = 2020-12-31
}}
== Accords et improvisation ==
Nous avons vu précédemment (chapitre ''[[../Gammes et intervalles#Modes et improvisation|Gammes et intervalles > Modes et improvisation]]'') que le choix d'un mode adapté permet d'improviser sur un accord. L'harmonisation des gammes permet, en inversant le processus, d'étendre notre palette : il suffit de repérer l'accord sur une harmonisaiton de gamme, et d'utiliser cette gamme-là, dans le mode correspondant du degré de l'accord (voir ci-dessus ''[[#Harmonisation par des accords de septième|Harmonisation par des accords de septième]]'').
Par exemple, nous avons vu que l'accord sur le septième degré d'une gamme majeure était un accord demi-diminué ; nous savons donc que sur un accord demi-diminué, nous pouvons improviser sur le mode correspondant au septième degré, soit le mode de ''si'' (locrien).
Un accord de septième de dominante étant commun aux deux tonalités homonymes (par exemple ''fa'' majeur et ''fa'' mineur pour un ''do''<sup>7</sup><sub>+</sub> / C<sup>7</sup>), nous pouvons utiliser le mode de ''sol'' de la gamme majeure (mixolydien) ou de la gamme mineure mineure (mode phrygien dominant, ou phrygien espagnol) pour improviser. Mais l'accord de septième de dominante est aussi l'accord au début d'une grille blues ; on peut donc improviser avec une gamme blues, même si la tierce est majeure dans l'accord et mineure dans la gamme.
[[Fichier:Mode improvisation accords do complet.svg]]
== Autres accords courants ==
[[fichier:Cluster cdefg.png|vignette|Agrégat ''do - ré - mi - fa - sol''.]]
Nous avons vu précédemment l'harmonisation des tonalités majeures et mineures harmoniques par des triades et des accords de septième ; certains accords étant rarement utilisés (l'accord sur le degré {{Times New Roman|III}} et, pour les tonalités mineures harmoniques, l'accord sur la tonique), certains accords étant utilisés comme des accords sur un autre degré (les accords sur la sensible étant considérés comme des accords de dominante sans fondamentale).
Dans l'absolu, on peut utiliser n'importe quelle combinaison de notes, jusqu'aux agrégats, ou ''{{lang|en|clusters}}'' (mot anglais signifiant « amas », « grappe ») : un ensemble de notes contigües, séparées par des intervalles de seconde. Dans la pratique, on reste souvent sur des accords composés de superpositions de tierces, sauf dans le cas de transitions (voir la section ''[[#Notes étrangères|Notes étrangère]]'').
=== En musique classique ===
On utilise parfois des accords dont les notes ne sont pas dans la tonalité (hors modulation). Il peut s'agir d'accords de passage, de notes étrangères, par exemple utilisant un chromatisme (mouvement conjoint par demi-tons).
Outre les accords de passage, les autres accords que l'on rencontre couramment en musique classique sont les accords de neuvième, et les accords de onzième et treizième sur tonique. Ces accords sont simplement obtenus en continuant à empiler les tierces. Il n'y a pas d'accord d'ordre supérieur car la quinzième est deux octaves au-dessus de la fondamentale.
Comme pour les accords de septième, on distingue les accords de neuvième de dominante et les accords de neuvième d'espèce. Dans le cas de la neuvième de dominante, il y a une différence entre les tonalités majeures et mineures : l'intervalle de neuvième est respectivement majeur et mineur. Les chiffrages des renversements peuvent donc différer. Comme pour les accords de septième de dominante, on considère que les accords de septième sur le degré {{Times New Roman|VI}} sont en fait des accords de neuvième de dominante sans fondamentale.
Les accords de neuvième d'espèce sont en général préparés et résolus. Préparés : la neuvième étant une note dissonante (c'est à une octave près la seconde de la fondamentale), l'accord qui précède doit contenir cette note, mais dans un accord consonant ; la neuvième est donc commune avec l'accord précédent. Résolus : la dissonance est résolue en abaissant la neuvième par un mouvement conjoint. Par exemple, en tonalité de ''do'' majeur, si l'on veut utiliser un accord de neuvième d'espèce sur la tonique ''(do - mi - sol - si - ré)'', on peut utiliser avant un accord de dominante ''(sol - si - ré)'' en préparation puis un accord parfait sur le degré {{Times New Roman|IV}} ''(fa - la - do)'' en résolution ; nous avons donc sur la voie la plus aigüe la succession ''ré'' (consonant) - ''ré'' (dissonant) - ''do'' (consonant).
On rencontre également parfois des accords de onzième et de treizième. On omet en général la tierce, car elle est dissonante avec la onzième. L'accord le plus fréquemment rencontré est l'accord sur la tonique : on considère alors que c'est un accord sur la dominante que l'on a enrichi « par le bas », en ajoutant une quinte inférieure. par exemple, dans la tonalité de ''do'' majeur, l'accord ''do - sol - si - ré - fa'' est considéré comme un accord de septième de dominante sur tonique, le degré étant noté « {{Times New Roman|V}}/{{Times New Roman|I}} ». De même pour l'accord ''do - sol - si - ré - fa - la'' qui est considéré comme un accord de neuvième de dominante sur tonique.
=== En jazz ===
En jazz, on utilise fréquemment l'accord de sixte à la place de l'accord de septième majeure sur la tonique. Par exemple, en ''do'' majeur, on utilise l'accord C<sup>6</sup> ''(do - mi - sol - la)'' à la place de C<sup>Δ</sup> ''(do - mi - sol - si)''. On peut noter que C<sup>6</sup> est un renversement de Am<sup>7</sup> et pourrait donc se noter Am<sup>7</sup>/C ; cependant, le fait de le noter C<sup>6</sup> indique que l'on a bien un accord sur la tonique qui s'inscrit dans la tonalité de ''do'' majeur (et non, par exemple, de ''la'' mineur naturelle) — par rapport à l'harmonie fonctionnelle, on remarquera que Am<sup>7</sup> a une fonction tonique, l'utilisation d'un renversement de Am<sup>7</sup> à la place d'un accord de C<sup>Δ</sup> est donc logique.
Les accords de neuvième, onzième et treizième sont utilisés comme accords de septième enrichis. Le chiffrage suit les règles habituelles : on ajoute un « 9 », un « 11 » ou un « 13 » au chiffrage de l'accord de septième.
On utilise également des accords dits « suspendus » : ce sont des accords de transition qui sont obtenus en prenant une triade majeure ou mineure et en remplaçant la tierce par la quarte juste (cas le plus fréquent) ou la seconde majeure. Plus particulièrement, lorsque l'on parle simplement « d'accord suspendu » sans plus de précision, cela désigne l'accord de neuvième avec une quarte suspendue, noté « 9sus4 » ou simplement « sus ».
== L'harmonie tonale ==
L'harmonie tonale est un ensemble de règle assez strictes qui s'appliquent dans la musique savante européenne, de la période baroque à la période classique classique ({{pc|xiv}}<sup>e</sup>-{{pc|xviii}}<sup>e</sup> siècle). Certaines règles sont encore largement appliquées dans divers styles musicaux actuels, y compris populaire (rock, rap…), d'autres sont au contraire ignorées (par exemple, un enchaînement de plusieurs accords de même qualité forme un mouvement parallèle, ce qui est proscrit en harmonie tonale). De nos jours, on peut voir ces règles comme des règles « de bon goût », et leur application stricte comme une manière de composer « à la manière de ».
Précédemment, nous avons vu la progression des accords. Ci-après, nous abordons aussi la manière dont les notes de l'accord sont réparties entre plusieurs voix, et comment on construit chaque voix.
=== Concepts fondamentaux ===
; Consonance
: Les intervalles sont considérés comme « plus ou moins consonants » :
:* consonance parfaite : unisson, quinte et octave ;
:* consonance mixte (parfaite dans certains contextes, imparfaite dans d'autres) : quarte ;
:* consonance imparfaite : tierce et sixte ;
:* dissonance : seconde et septième.
; Degrés
: Certains degrés sont considérés comme « forts », « meilleurs », ce sont les « notes tonales » : {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (sous-dominante) et {{Times New Roman|V}} (dominante).
[[Fichier:Mouvements harmoniques.svg|vignette|upright=0.75|Mouvements harmoniques.]]
; Mouvements
: Le mouvement décrit la manière dont les voix évoluent les unes par rapport aux autres :
:# Mouvement parallèle : les voix sont séparées par un intervalle constant.
:# Mouvement oblique : une voix reste constante, c'est le bourdon ; l'autre monte ou descend.
:# Mouvement contraire : une voix descend, l'autre monte.
:# Échange de voix : les voix échangent de note ; les mélodies se croisent mais on a toujours le même intervalle harmonique.
{{clear}}
=== Premières règles ===
; Règle du plus court chemin
: Quand on passe d'un accord à l'autre, la répartition des notes se fait de sorte que chaque voix fait le plus petit mouvement possible. Notamment : si les deux accords ont des notes en commun, alors les voix concernées gardent la même note.
: Les deux voix les plus importantes sont la voix aigüe — soprano — et la voix la plus grave — basse. Ces deux voix sont relativement libres : la voix de soprano a la mélodie, la voix de basse fonde l'harmonie. La règle du plus court chemin s'applique surtout aux voix intermédiaires ; si l'on a des mouvements conjoints, ou du moins de petits intervalles — c'est le sens de la règle du plus court chemin —, alors les voix sont plus faciles à interpréter. Cette règle évite également que les voix n'empiètent l'une sur l'autre (voir la règle « éviter le croisement des voix »).
; Éviter les consonances parfaites consécutives
:* Lorsque deux voix sont à l'unisson ou à l'octave, elles ne doivent pas garder le même intervalle, l'effet serait trop plat.
:* Lorsque deux voix sont à la quarte ou à la quinte, elles ne doivent pas garder le même intervalle, car l'effet est trop dur.
: Pour éviter cela, lorsque l'on part d'un intervalle juste, on a intérêt à pratiquer un mouvement contraire aux voix qui ne gardent pas la même note, ou au moins un mouvement direct : les voix vont dans le même sens, mais l'intervalle change.
: Notez que même avec le mouvement contraire, on peut avoir des consonances parfaites consécutives, par exemple si une voix fait ''do'' aigu ↗ ''sol'' aigu et l'autre ''sol'' médium ↘ ''do'' grave.
: L'interdiction des consonances parfaites consécutives n'a pas été toujours appliquée, le mouvement parallèle strict a d'ailleurs été le premier procédé utilisé dans la musique religieuse au {{pc|x}}<sup>e</sup> siècle. On peut par exemple utiliser des quintes parallèles pour donner un style médiéval au morceau. On peut également utiliser des octaves parallèles sur plusieurs notes afin de créer un effet de renforcement de la mélodie.
: Par ailleurs, les consonances parfaites consécutives sont acceptées lorsqu'il s'agit d'une cadence (transition entre deux parties ou bien conclusion du morceau).
; Éviter le croisement des voix
: Les voix sont organisées de la plus grave à la plus aigüe. Deux voix n'étant pas à l'unisson, celle qui est plus aigüe ne doit pas devenir la plus grave et ''vice versa''.
; Soigner la partie soprano
: Comme c'est celle qu'on entend le mieux, c'est en général celle qui porte la mélodie principale. On lui applique des règles spécifiques :
:# Si elle chante la sensible dans un accord de dominante ({{Times New Roman|V}}), alors elle doit monter à la tonique, c'est-à-dire que la note suivante sera la tonique située un demi-ton au dessus.
:# Si l'on arrive à une quinte ou une octave entre les parties basse et soprano par un mouvement direct, alors sur la partie soprano, le mouvement doit être conjoint. On doit donc arriver à cette situation par des notes voisines au soprano.
; Préférer certains accords
: Les deux degrés les plus importants sont la tonique ({{Times New Roman|I}}) et la dominante ({{Times New Roman|V}}), les accords correspondants ont donc une importance particulière.
: À l'inverse, l'accord de sensible ({{Times New Roman|VII}}) n'est pas considéré comme ayant une fonction harmonique forte. On le considère comme un accord de dominante affaibli. En tonalité mineure, on évite également l'accord de médiante ({{Times New Roman|III}}).
: Donc on utilise en priorité les accords de :
:# {{Times New Roman|I}} et {{Times New Roman|V}}.
:# Puis {{Times New Roman|II}}, {{Times New Roman|IV}}, {{Times New Roman|VI}} ; et {{Times New Roman|III}} en mode majeur.
:# On évite {{Times New Roman|VII}} ; et {{Times New Roman|III}} en mode mineur.
; Préférer certains enchaînements
: Les enchaînements d'accord peuvent être classés par ordre de préférence. Par ordre de préférence décroissante (du « meilleur » au « moins bon ») :
:# Meilleurs enchaînements : quarte ascendante ou descendante. Notons que la quarte est le renversement de la quinte, on a donc des enchaînements stables et naturels, mais avec un intervalle plus court qu'un enchaînement de quintes.
:# Bons enchaînements : tierce ascendante ou descendante. Les accords consécutifs ont deux notes en commun.
:# Enchaînements médiocres : seconde ascendante ou descendante. Les accords sont voisins, mais ils n'ont aucune note en commun. On les utilise de préférence en mouvement ascendant, et on utilise surtout les enchaînements {{Times New Roman|IV}}-{{Times New Roman|V}}, {{Times New Roman|V}}-{{Times New Roman|VI}} et éventuellement {{Times New Roman|I}}-{{Times New Roman|II}}.
:# Les autres enchaînements sont à éviter.
: On peut atténuer l'effet d'un enchaînement médiocre en plaçant le second accord sur un temps faible ou bien en passant par un accord intermédiaire.
[[Fichier:Progression Vplus4 I6.svg|thumb|Résolution d'un accord de triton (quarte sensible) vers l'accord de sixte de la tonique.]]
; La septième descend par mouvement conjoint
: Dans un accord de septième de dominante, la septième — qui est donc le degré {{Times New Roman|IV}} — descend par mouvement conjoint — elle est donc suivie du degré {{Times New Roman|III}}.
: Corolaire : un accord {{Times New Roman|V}}<sup>+4</sup> se résout par un accord {{Times New Roman|I}}<sup>6</sup> : on a bien un enchaînement {{Times New Roman|V}} → {{Times New Roman|I}}, et la 7{{e}} (degré {{Times New Roman|IV}}), qui est la basse de l'accord {{Times New Roman|V}}<sup>+4</sup>, descend d'un degré pour donner la basse de l'accord {{Times New Roman|I}}<sup>6</sup> (degré {{Times New Roman|III}}).
{{clear}}
[[Fichier:Progression I64 V7plus I5.svg|thumb|Accord de sixte et de quarte cadentiel.]]
; Un accord de sixte et quarte est un accord de passage
: Le second renversement d'un accord parfait est soit une appoggiature, soit un accord de passage, soit un accord de broderie.
: S'il s'agit de l'accord de tonique {{Times New Roman|I}}<sup>6</sup><sub>4</sub>, c'est « accord de sixte et quarte de cadence », l'appoggiature de l'accord de dominante de la cadence parfaite.
{{clear}}
Mais il faut appliquer ces règles avec discernement. Par exemple, la voix la plus aigüe est celle qui s'entend le mieux, c'est donc elle qui porte la mélodie principale. Il est important qu'elle reste la plus aigüe. La voix la plus grave porte l'harmonie, elle pose les accords, il est donc également important qu'elle reste la plus grave. Ceci a deux conséquences :
# Ces deux voix extrêmes peuvent avoir des intervalles mélodiques importants et donc déroger à la règle du plus court chemin : la voix aigüe parce que la mélodie prime, la voix de basse parce que la progression d'accords prime.
# Les croisements des voix intermédiaires sont moins critiques.
Par ailleurs, si l'on applique strictement toutes les règles « meilleurs accords, meilleurs enchaînements », on produit un effet conventionnel, stéréotypé. Il est donc important d'utiliser les solutions « moins bonnes », « médiocres » pour apporter de la variété.
Ajoutons que les renversements d'accords permettent d'avoir plus de souplesse : on reste sur le même accord, mais on enrichit la mélodie sur chaque voix.
Le ''Bolero'' de Maurice Ravel (1928) brise un certain nombre de ces règles. Par exemple, de la mesure 39 à la mesure 59, la harpe joue des secondes. De la mesure 149 à la mesure 165, les piccolo jouent à la sixte, dans des mouvement strictement parallèle, ce qui donne d'ailleurs une sonorité étrange. À partir de la mesure 239, de nombreux instruments jouent en mouvement parallèles (piccolos, flûtes, hautbois, cor, clarinettes et violons).
=== Application ===
[[Fichier:Harmonisation possible de frere jacques exercice.svg|vignette|Exercice : harmoniser ''Frère Jacques''.]]
Harmoniser ''Frère Jacques''.
Nous considérons un morceau à quatre voix : basse, ténor, alto et soprano. La soprano chante la mélodie de ''Frère Jacques''. L'exercice consiste à proposer l'écriture des trois autres voix en respectant les règles énoncées ci-dessus. Pour simplifier, nous ajoutons les contraintes suivantes :
* toutes les voix chantent des blanches ;
* nous nous limitons aux accords de quinte (accords de trois sons composés d'une tierce et d'une quinte) sans avoir recours à leurs renversements (accords de sixte, accords de sixte et de quarte).
Les notes à gauche de la portée indiquent la tessiture (ou ambitus), l'amplitude que peut chanter la voix.
{{clear}}
{{boîte déroulante/début|titre=Solution possible}}
[[Fichier:Harmonisation possible de frere jacques solution.svg|vignette|Harmonisation possible de ''Frère Jacques'' (solution de l'exercice).]]
Il n'y a pas qu'une solution possible.
Le premier accord doit contenir un ''do''. Nous sommes manifestement en tonalité de ''do'' majeur, nous proposons de commencer par l'accord parfait de ''do'' majeur, I<sup>5</sup>.
Le deuxième accord doit comporter un ''ré''. Si nous utilisons l'accord de quinte de ''ré'', nous allons créer une quinte parallèle. Nous pourrions utiliser un renversement, mais nous nous imposons de chercher un autre accord. Il peut s'agir de l'accord ''si''<sup>5</sup> ''(si-ré-fa)'' ou de l'accord de ''sol''<sup>5</sup> ''(sol-si-ré)''. La dernière solution permet d'utiliser l'accord de dominante qui est un accord important de la tonalité. La règle du plus court chemin imposerait le ''sol'' grave pour la partie de basse, mais cela est proche de la limite du chanteur, nous préférons passer au ''sol'' aigu, plus facile à chanter. Nous vérifions qu'il n'y a pas de quinte parallèle : l'intervalle ascendant ''do-sol'' (basse-alto) devient ''sol-si'' (3<sup>ce</sup>), l'intervalle descendant ''do-sol'' (soprano-alto) devient ''ré-si'' (3<sup>ce</sup>).
De la même manière, pour le troisième accord, nous ne pouvons pas passer à un accord de ''la''<sup>5</sup> pour éviter une quinte parallèle. Nous avons le choix entre ''do''<sup>5</sup> ''(do-mi-sol)'' et ''mi''<sup>5</sup> ''(mi-sol-si)''. Nous préférons revenir à l'accord de fondamental, solution très stable (l'enchaînement {{Times New Roman|V}}-{{Times New Roman|I}} formant une cadence parfaite).
Pour le quatrième accord, nous pourrions rester sur l'accord parfait de ''do'' mais cela planterait en quelque sorte la fin du morceau puisque l'on resterait sur la cadence parfaite ; or, nous connaissons le morceau et savons qu'il n'est pas fini. Nous choisissons l'accord de ''la''<sup>5</sup> qui est une sixte ascendante ({{Times New Roman|I}}-{{Times New Roman|VI}}).
Nos aurions pu répartir les voix différemment. Par exemple :
* alto : ''sol''-''si''-''sol''-''do'' ;
* ténor : ''mi''-''ré''-''mi''-''mi''.
{{boîte déroulante/fin}}
[[Fichier:Harmonisation possible de frere jacques.midi|vignette|Fichier son correspondant.]]
{{clear}}
== Annexe ==
=== Accords en musique classique ===
Un accord est un ensemble de notes jouées simultanément. Il peut s'agir :
* de notes jouées par plusieurs instruments ;
* de notes jouées par un même instrument : piano, clavecin, orgue, guitare, harpe (la plupart des instruments à clavier et des instruments à corde).
Pour deux notes jouées simultanément, on parle d'intervalle « harmonique » (par opposition à l'intervalle « mélodique » qui concerne les notes jouées successivement).
Les notes répétées à différentes octaves ne changent pas la nature de l'accord.
La musique classique considère en général des empilements de tierces ; un accord de trois notes sera constitué de deux tierces successives, un accord de quatre notes de trois tierces…
Lorsque tous les intervalles sont des intervalles impairs — tierces, quintes, septièmes, neuvièmes, onzièmes, treizièmes… — alors l'accord est dit « à l'état fondamental » (ou encore « primitif » ou « direct »). La note de la plus grave est appelée « fondamentale » de l'accord. Lorsque l'accord comporte un ou des intervalles pairs, l'accord est dit « renversé » ; la note la plus grave est appelée « basse ».
De manière plus générale, l'accord est dit à l'état fondamental lorsque la basse est aussi la fondamentale. On a donc un état idéal de l'accord (état canonique) — un empilement strict de tierces — et l'état réel de l'accord — l'empilement des notes réellement jouées, avec d'éventuels redoublements, omissions et inversions ; et seule la basse indique si l'accord est à l'état fondamental ou renversé.
Le chiffrage dit de « basse continue » ''({{lang|it|basso continuo}})'' désigne la représentation d'un accord sous la forme d'un ou plusieurs chiffres arabes et éventuellement d'un chiffre romain.
==== Accords de trois notes ====
En musique classique, les seuls accords considérés comme parfaitement consonants, c'est-à-dire sonnant agréablement à l'oreille, sont appelés « accords parfaits ». Si l'on prend une tonalité et un mode donné, alors l'accord construit par superposition es degrés I, III et V de cette gamme porte le nom de la gamme qui l'a généré.
[[fichier:Accord do majeur chiffre.svg|vignette|upright=0.5|Accord parfait de ''do'' majeur chiffré.]]
Par exemple :
* « l'accord parfait de ''do'' majeur » est composé des notes ''do'', ''mi'' et ''sol'' ;
* « l'accord parfait de ''la'' mineur » est composé des notes ''la'', ''do'' et ''mi''.
Un accord parfait majeur est donc composé, en partant de la fondamentale, d'une tierce majeure et d'une quinte juste. Un accord parfait mineur est composé d'une tierce mineure et d'une quinte juste.
L'accord parfait à l'état fondamental est appelé « accord de quinte » et est simplement chiffré « 5 » pour indiquer la quinte.
On peut également commencer un accord sur sa deuxième ou sa troisième note, en faisant monter celle(s) qui précède(nt) à l'octave suivante. On parle alors de « renversement d'accord » ou d'accord « renversé ».
[[Fichier:Accord do majeur renversements chiffre.svg|vignette|upright=0.75|Accord parfait de ''do'' majeur et ses renversements, chiffrés.]]
Par exemple,
* le premier renversement de l'accord parfait de ''do'' majeur est :<br /> ''mi'', ''sol'', ''do'' ;
* le second renversement de l'accord parfait de do majeur est :<br /> ''sol'', ''do'', ''mi''.
Les notes conservent leur nom de « fondamentale », « tierce » et « quinte » malgré le changement d'ordre. La note la plus grave est appelée « basse ».
Dans le cas du premier renversement, le deuxième note est la tierce de la basse (la note la plus grave) et la troisième note est la sixte ; le chiffrage en chiffres arabes est donc « 6 » (puisque l'on omet la tierce) et l'accord est appelé « accord de sixte ». Pour le deuxième renversement, les intervalles sont la quarte et la sixte, le chiffrage est donc « 6-4 » et l'accord est appelé « accord de sixte et de quarte ».
Dans tous les cas, on chiffre le degré on considérant la fondamentale, par exemple {{Times New Roman|I}} si l'accord est construit sur la tonique de la gamme.
Les autres accords de trois notes que l'on rencontre sont :
* l'accord de quinte diminuée, constitué d'une tierce mineure et d'une quinte diminuée ; lorsqu'il est construit sur le septième degré d'une gamme, on considère que c'est un accord de septième de dominante sans fondamentale (voir plus bas), le degré est donc indiqué « “{{Times New Roman|V}}” » (cinq entre guillemets) et non « {{Times New Roman|VII}} » ;
* l'accord de quinte augmenté : il est composé d'une tierce majeure et qu'une quinte augmentée.
Dans le tableau ci-dessous,
* « m » désigne un intervalle mineur ;
* « M » un intervalle majeur ou le mode majeur ;
* « J » un intervalle juste ;
* « d » un intervalle diminué ;
* « A » un intervalle augmenté ;
* « mh » le mode mineur harmonique ;
* « ma » le mode mineur ascendant ;
* « md » le mode mineur descendant.
{| class="wikitable"
|+ Accords de trois notes
! scope="col" rowspan="2" | Nom
! scope="col" rowspan="2" | 3<sup>ce</sup>
! scope="col" rowspan="2" | 5<sup>te</sup>
! scope="col" rowspan="2" | État fondamental
! scope="col" rowspan="2" | 1<sup>er</sup> renversement
! scope="col" rowspan="2" | 2<sup>nd</sup> renversement
! scope="col" colspan="4"| Construit sur les degrés
|-
! scope="col" | M
! scope="col" | mh
! scope="col" | ma
! scope="col" | md
|-
| Accord parfait<br /> majeur || M || J
| accord de quinte || accord de sixte || accord de<br />sixte et de quarte
| {{Times New Roman|I, IV, V}} || {{Times New Roman|V, VI}} || {{Times New Roman|IV, V}} || {{Times New Roman|III, VI, VII}}
|-
| Accord parfait<br /> mineur || m || J
| accord de quinte || accord de sixte || accord de<br />sixte et de quarte
| {{Times New Roman|II, III, VI}} || {{Times New Roman|I, IV}} || {{Times New Roman|I, II}} || {{Times New Roman|I, IV, V}}
|-
| Accord de<br />quinte diminuée || m || d
| accord de<br />quinte diminuée || accord de<br />sixte sensible<br />sans fondamentale || accord de triton<br />sans fondamentale
| {{Times New Roman|VII (“V”)}} || {{Times New Roman|II, VII (“V”)}} || {{Times New Roman|VI, VII (“V”)}} || {{Times New Roman|II}}
|-
| Accord de<br />quinte augmentée || M || A
| accord de<br />quinte augmentée || accord de sixte<br />et de tierce sensible || accord de sixte et de quarte<br />sur sensible
| || {{Times New Roman|III}} || {{Times New Roman|III}} ||
|}
==== Accords de quatre notes ====
Les accords de quatre notes sont des accord composés de trois tierces superposées. La dernière note étant le septième degré de la gamme, on parle aussi d'accords de septième.
Ces accords sont dissonants : ils contiennent un intervalle de septième (soit une octave montante suivie d'une seconde descendante). Ils laissent donc une impression de « tension ».
Il existe sept différents types d'accords, ou « espèces ». Citons l'accord de septième de dominante, l'accord de septième mineure et l'accord de septième majeure.
===== L'accord de septième de dominante =====
[[Fichier:Accord 7e dominante do majeur renversements chiffre.svg|vignette|Accord de septième de dominante de ''do'' majeur et ses renversements, chiffrés.]]
L'accord de septième de dominante est l'empilement de trois tierces à partir de la dominante de la gamme, c'est-à-dire du {{Times New Roman|V}}<sup>e</sup> degré. Par exemple, l'accord de septième de dominante de ''do'' majeur est l'accord ''sol''-''si''-''ré''-''fa'', et l'accord de septième de dominante de ''la'' mineur est ''mi''-''sol''♯-''si''-''ré''. L'accord de septième de dominante dont la fondamentale est ''do'' (''do''-''mi''-''sol''-''si''♭) appartient à la gamme de ''fa'' majeur.
Que le mode soit majeur ou mineur, il est composé d'une tierce majeure, d'une quinte juste et d'une septième mineure (c'est un accord parfait majeur auquel on ajoute une septième mineure). C'est de loin l'accord de septième le plus utilisé ; il apparaît au {{pc|xvii}}<sup>e</sup> en musique classique.
Dans son état fondamental, son chiffrage est {{Times New Roman|V 7/+}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}). Le signe plus indique la sensible.
Son premier renversement est appelé « accord de quinte diminuée et sixte » et est noté {{Times New Roman|V 6/<s>5</s>}} (ou {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}).
Son deuxième renversement est appelé « accord de sixte sensible », puisque la sixte de l'accord est la sensible de la gamme, et est noté {{Times New Roman|V +6}} (ou {{Times New Roman|V<sup>+6</sup>}}).
Son troisième renversement est appelé « accord de quarte sensible » et est noté {{Times New Roman|V +4}} (ou {{Times New Roman|V<sup>+4</sup>}}).
[[Fichier:Accord 7e dominante sans fondamentale do majeur renversements chiffre.svg|vignette|Accord de septième de dominante sans fondamentale de ''do'' majeur et ses renversements, chiffrés.]]
On utilise aussi l'accord de septième de dominante sans fondamentale ; c'est alors un accord de trois notes.
Dans son état fondamental, c'est un « accord de quinte diminuée » placé sur le {{Times New Roman|VII}}<sup>e</sup> degré (mais c'est bien un accord construit sur le {{Times New Roman|V}}<sup>e</sup> degré), noté {{Times New Roman|“V” <s>5</s>}} (ou {{Times New Roman|“V”<sup><s>5</s></sup>}}). Notez les guillemets qui indiquent que la fondamentale V est absente.
Dans son premier renversement, c'est un « accord de sixte sensible sans fondamentale » noté {{Times New Roman|“V” +6/3}} (ou {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}}).
Dans son second renversement, c'est un « accord de triton sans fondamentale » (puisque le premier intervalle est une quarte augmentée qui comporte trois tons) noté {{Times New Roman|“V” 6/+4}} (ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}).
Notons qu'un accord de septième de dominante n'a pas toujours la dominante pour fondamentale : tout accord composé d'une tierce majeure, d'une quinte juste et d'une septième mineure est un accord de septième de dominante et est chiffré {{Times New Roman|<sup>7</sup><sub>+</sub>}}, quel que soit le degré sur lequel il est bâti (certaines notes peuvent avoir une altération accidentelle).
===== Les accords de septième d'espèce =====
Les autres accords de septièmes sont dits « d'espèce ».
L'accord de septième mineure est l'accord de septième formé sur la fondamentale d'une gamme mineure ''naturelle''. Par exemple, l'accord de septième mineure de ''la'' est ''la''-''do''-''mi''-''sol''. Il est composé d'une tierce mineure, d'une quinte juste et d'une septième mineure (c'est un accord parfait mineur auquel on ajoute une septième mineure).
L'accord de septième majeure est l'accord de septième formé sur la fondamentale d'une gamme majeure. Par exemple, L'accord de septième majeure de ''do'' est ''do''-''mi''-''sol''-''si''. Il est composé d'une tierce majeure, d'une quinte juste et d'une septième majeure (c'est un accord parfait majeur auquel on ajoute une septième majeure).
==== Utilisation du chiffrage ====
Le chiffrage est utilisé de deux manières.
La première manière, c'est la notation de la basse continue. La basse continue est une technique d'improvisation utilisée dans le baroque pour l'accompagnement d'instruments solistes. Sur la partition, on indique en général la note de basse de l'accord et le chiffrage en chiffres arabes.
La seconde manière, c'est pour l'analyse d'une partition. Le fait de chiffrer les accords permet de mieux en comprendre la structure.
De manière générale, on peut retenir que :
* le chiffrage « 5 » indique un accord parfait, superposition d'une tierce (majeure ou mineure) et d'une quinte juste ;
* le chiffrage « 6 » indique le premier renversement d'un accord parfait ;
* le chiffrage « 6/4 » indique le second renversement d'un accord parfait ;
* chiffrage « 7/+ » indique un accord de septième de dominante ;
* le signe « + » indique en général que la note de l'intervalle est la sensible ;
* un intervalle barré désigne un intervalle diminué.
[[fichier:Accords gamme do majeur la mineur.svg|class=transparent| center | Principaux accords construits sur les gammes de ''do'' majeur et de ''la'' mineur harmonique.]]
=== Notation « jazz » ===
En jazz et de manière générale en musique rock et populaire, la base d'un accord est la triade composée d'une tierce (majeure ou mineure) et d'une quinte juste. Pour désigner un accord, on utilise la note fondamentale, éventuellement désigné par une lettre dans le système anglo-saxon (A pour ''la'' etc.), suivi d'une qualité (comme « m », « + »…).
Les renversements ne sont pas notés de manière particulière, ils sont notés comme les formes fondamentales.
Dans les deux tableaux suivants, la fondamentale est notée X (remplace le C pour un accord de ''do'', le D pour un accord de ''ré''…). La construction des accords est décrite par la suite.
[[Fichier:Arbre accords triades 5d5J5A.svg|vignette|upright=1.5|Formation des triades présentée sous forme d'arbre.]]
{| class="wikitable"
|+ Notation des principales triades
|-
|
! scope="col" | Tierce<br />mineure (3m)
! scope="col" | Tierce<br />majeure (3M)
|-
! scope="row" | Quinte diminuée (5d)
| X<sup>o</sup>, Xm<sup>♭5</sup>, X–<sup>♭5</sup> ||
|-
! scope="row" | Quinte juste (5J)
| Xm, X– || X
|-
! scope="row" | Quinte augmentée (5A)
| || X+, X<sup>♯5</sup>
|}
[[Fichier:Triades do.svg|class=transparent|center|Triades de do.]]
{| class="wikitable"
|+ Notation des principaux accords de septième
|-
| colspan="2" |
! scope="col" | Tierce<br />mineure (3m)
! scope="col" | Tierce<br />majeure (3M)
|-
! scope="row" rowspan="2" | Quinte<br />diminuée (5d)
! scope="row" | Septième diminuée (7d)
| X<sup>o7</sup> ||
|-
! scope="row" | Septième mineure (7m)
| Xm<sup>7(♭5)</sup>, X–<sup>7(♭5)</sup>, X<sup>Ø</sup> ||
|-
! scope="row" rowspan="3" | Quinte<br />juste (5J)
! scope="row" | Sixte majeure (6M)
| Xm<sup>6</sup> || X<sup>6</sup>
|-
! scope="row" | Septième mineure (7m)
| Xm<sup>7</sup>, X–<sup>7</sup> || X<sup>7</sup>
|-
! scope="row" | Septième majeure (7M)
| Xm<sup>maj7</sup>, X–<sup>maj7</sup>, Xm<sup>Δ</sup>, X–<sup>Δ</sup> || X<sup>maj7</sup>, X<sup>Δ</sup>
|-
! scope="row" rowspan="2" | Quinte<br />augmentée (5A)
! scope="row" | Septième mineure (7m)
| || X+<sup>7</sup>
|-
! scope="row" | Septième majeure (7M)
| || X+<sup>maj7</sup>
|}
[[Fichier:Arbre accords septieme.svg|class=transparent|center|Formation des accords de septième présentée sous forme d'arbre.]]
[[Fichier:Accords do septieme.svg|class=transparent|center|Accord de do septième.]]
On notera que l'intervalle de sixte majeure est l'enharmonique de celui de septième diminuée (6M = 7d).
[[File:Principaux accords do.svg|class=transparent|center|Principaux accords de do.]]
==== Triades ====
; Accords fondés sur une tierce majeure
* accord parfait majeur : pas de notation
*: p. ex. « ''do'' » ou « C » pour l'accord parfait de ''do'' majeur (''do'' - ''mi'' - ''sol'')
; Accords fondés sur une tierce mineure
* accord parfait mineur : « m », « min » ou « – »
*: « ''do'' m », « ''do'' – », « Cm », « C– »… pour l'accord parfait de ''do'' mineur (''do'' - ''mi''♭ - ''sol'')
==== Triades modifiées ====
; Accords fondés sur une tierce majeure
* accord augmenté (la quinte est augmentée) : aug, +, ♯5
*: « ''do'' aug », « ''do'' + », « ''do''<sup>♯5</sup> » « Caug », « C+ » ou « C<sup>♯5</sup> » pour l'accord de ''do'' augmenté (''do'' - ''mi'' - ''sol''♯)
: L'accord augmenté est un empilement de tierces majeures. Ainsi, un accord augmenté a deux notes communes avec deux autres accords augmentés : C+ (''do'' - ''mi'' - ''sol''♯) a deux notes communes avec A♭+ (''la''♭ - ''do'' - ''mi'') et avec E+ (''mi'' - ''sol''♯ - ''si''♯) ; et on remarque que ces trois accords sont en fait enharmoniques (avec les enharmonies ''la''♭ = ''sol''♯ et ''si''♯ = ''do''). En effet, l'octave comporte six tons (sous la forme de cinq tons et deux demi-tons), et une tierce majeure comporte deux tons, on arrive donc à l'octave en ajoutant une tierce majeure à la dernière note de l'accord.
; Accords fondés sur une tierce mineure
* accord diminué (la quinte est diminuée) : dim, o, ♭5
*: « ''do'' dim », « ''do''<sup>o</sup> », « ''do''<sup>♭5</sup> », « Cdim », « C<sup>o</sup> » ou « C<sup>♭5</sup> » pour l'accord de ''do'' diminuné (''do'' - ''mi''♭ - ''sol''♭)
: On remarque que la quinte diminuée est l'enharmonique de la quarte augmentée et est l'intervalle appelé « triton » (car composé de trois tons).
; Accords fondés sur une tierce majeure ou mineure
* accord suspendu de seconde : la tierce est remplacée par une seconde majeure : sus2
*: « ''do''<sup>sus2</sup> » ou « C<sup>sus2</sup> » pour l'accord de ''do'' majeur suspendu de seconde (''do''-''ré''-''sol'')
* accord suspendu de quarte : la tierce est remplacée par une quarte juste : sus4
*: « ''do''<sup>sus4</sup> » ou « C<sup>sus4</sup> » pour l'accord de ''do'' majeur suspendu de quarte (''do''-''fa''-''sol'')
==== Triades appauvries ====
; Accords fondés sur une tierce majeure ou mineure
* accord de puissance : la tierce est omise, l'accord n'est constitué que de la fondamentale et de la quinte juste : 5
*: « ''do''<sup>5</sup> », « C<sup>5</sup> » pour l'accord de puissance de ''do'' (''do'' - ''la'')
{{note|Très utilisé dans les musiques rock, hard rock et heavy metal, il est souvent joué renversé (''la'' - ''do'') ou bien avec l'ajout de l'octave (''do'' - ''la'' - ''do'').}}
==== Triades enrichies ====
; Accords fondés sur une tierce majeure
* accord de septième (la 7<sup>e</sup> est mineure) : 7
*: « ''do''<sup>7</sup> », « C<sup>7</sup> » pour l'accord de ''do'' septième, appelé « accord de septième de dominante de ''fa'' majeur » en musique classique (''do'' - ''mi'' - ''sol'' - ''si''♭)
* accord de septième majeure : Δ, 7M ou maj7
*: « ''do'' <sup>Δ</sup> », « ''do'' <sup>maj7</sup> », « C<sup>Δ</sup> », « C<sup>7M</sup> »… pour l'accord de ''do'' septième majeure (''do'' - ''mi'' - ''sol'' - ''si'')
; Accords fondés sur une tierce mineure
* accord de mineur septième (la tierce et la 7<sup>e</sup> sont mineures) : m7, min7 ou –7
*: « ''do'' m<sup>7</sup> », « ''do'' –<sup>7</sup> », « Cm<sup>7</sup> », « C–<sup>7</sup> »… pour l'accord de ''do'' mineur septième, appelé « accord de septième de dominante de ''fa'' mineur » en musique classique (''do'' - ''mi''♭ - ''sol'' - ''si''♭)
* accord mineure septième majeure : m7M, m7maj, mΔ, –7M, –7maj, –Δ
*: « ''do'' m<sup>7M</sup> », « ''do'' m<sup>maj7</sup> », « ''do'' –<sup>Δ</sup> », « Cm<sup>7M</sup> », « Cm<sup>maj7</sup> », « C–<sup>Δ</sup> »… pour l'accord de ''do'' mineur septième majeure (''do'' - ''mi''♭ - ''sol'' - ''si'')
* accord de septième diminué (la quinte et la septième sont diminuée) : dim 7 ou o7
*: « ''do'' dim<sup>7</sup> », « ''do''<sup>o7</sup> », « Cdim<sup>7</sup> » ou « C<sup>o7</sup> » pour l'accord de ''do'' septième diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si''♭)
* accord demi-diminué (seule la quinte est diminuée, la septième est mineure) : Ø ou –7(♭5)
*: « ''do''<sup>Ø</sup> », « ''do''<sup>7(♭5)</sup> », « C<sup>Ø</sup> » ou « C<sup>7♭5</sup> » pour l'accord de ''do'' demi-diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si'')
=== Construction pythagoricienne des accords ===
Nous avons vu au débuts que lorsque l'on joue deux notes en même temps, leurs vibrations se superposent. Certaines superpositions créent un phénomène de battement désagréable, c'est le cas des secondes.
Dans le cas d'une tierce majeure, les fréquences des notes quadruple et quintuple d'une même base : les fréquences s'écrivent 4׃<sub>0</sub> et 5׃<sub>0</sub>. Cette superposition de vibrations est agréable à l'oreille. Nous avons également vu que dans le cas d'une quinte juste, les fréquences sont le double et le triple d'une même base, ou encore le quadruple et sextuple si l'on considère la moitié de cette base.
Ainsi, dans un accord parfait majeur, les fréquences des fondamentales des notes sont dans un rapport 4, 5, 6. De même, dans le cas d'un accord parfait mineur, les proportions sont de 1/6, 1/5 et 1/4.
{{voir|[[../Caractéristiques_et_notation_des_sons_musicaux#Construction_pythagoricienne_et_gamme_de_sept_tons|Caractéristiques et notation des sons musicaux > Construction pythagoricienne et gamme de sept tons]]}}
=== Un peu de physique : interférences ===
Les sons sont des vibrations. Lorsque l'on émet deux sons ou plus simultanément, les vibrations se superposent, on parle en physique « d'interférences ».
Le modèle le plus simple pour décrire une vibration est la [[w:fr:Fonction sinus|fonction sinus]] : la pression de l'air P varie en fonction du temps ''t'' (en secondes, s), et l'on a pour un son « pur » :
: P(''t'') ≈ sin(2π⋅ƒ⋅''t'')
où ƒ est la fréquence (en hertz, Hz) du son.
Si l'on émet deux sons de fréquence respective ƒ<sub>1</sub> et ƒ<sub>2</sub>, alors la pression vaut :
: P(''t'') ≈ sin(2π⋅ƒ<sub>1</sub>⋅''t'') + sin(2π⋅ƒ<sub>2</sub>⋅''t'').
Nous avons ici une [[w:fr:Identité trigonométrique#Transformation_de_sommes_en_produits,_ou_antilinéarisation|identité trigonométrique]] dite « antilinéarisation » :
: <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{f_1 + f_2}{2}t \right ) \cdot \sin \left ( 2\pi \frac{f_1 - f_2}{2}t \right ).</math>
On peut étudier simplement deux situations simples.
[[Fichier:Battements interferentiels.png|vignette|Deux sons de fréquences proches créent des battements : la superposition d'une fréquence et d'une enveloppe.]]
La première, c'est quand les fréquences ƒ<sub>1</sub> et ƒ<sub>2</sub> sont très proches. Alors, la moyenne (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2 est très proche de ƒ<sub>1</sub> et ƒ<sub>2</sub> ; et la demie différence (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2 est très proche de zéro. On a donc une enveloppe de fréquence très faible, (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2, dans laquelle s'inscrit un son de fréquence moyenne, (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2. C'est cette enveloppe de fréquence très faible qui crée les battements, désagréables à l'oreille.
Sur l'image ci-contre, le premier trait rouge montre un instant où les vibrations sont opposées ; elles s'annulent, le son s'éteint. Le second trait rouge montre un instant où les vibrations sont en phase : elle s'ajoutent, le son est au plus fort.
{{clear}}
La seconde, c'est lorsque les deux fréquences sont des multiples entiers d'une même fréquence fondamentale ƒ<sub>0</sub> : ƒ<sub>1</sub> = ''n''<sub>1</sub>⋅ƒ<sub>0</sub> et ƒ<sub>0</sub> = ''n''<sub>0</sub>⋅ƒ<sub>0</sub>. On a alors :
: <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{n_1 + n_2}{2}f_0 \cdot t \right ) \cdot \sin \left ( 2\pi \frac{n_1 - n_2}{2}f_0 \cdot t \right ).</math>
On multiplie donc deux fonctions qui ont des fréquences multiples de ƒ<sub>0</sub>. La différence minimale entre ''n''<sub>1</sub> et ''n''<sub>2</sub> vaut 1 ; on a donc une enveloppe dont la fréquence est au minimum la moitié de ƒ<sub>0</sub>, c'est-à-dire un son une octave en dessous de ƒ<sub>0</sub>. Donc, cette enveloppe ne crée pas d'effet de battement, ou plutôt, le battement est trop rapide pour être perçu comme tel. Dans cette enveloppe, on a une fonction sinus dont la fréquence est également un multiple de ƒ<sub>0</sub> ; l'enveloppe et la fonction qui y est inscrite ont donc de nombreux « points communs », d'où l'effet harmonieux.
=== Le tonnetz ===
[[File:Speculum musicae.png|thumb|right|225px|Euler, ''De harmoniæ veris principiis'', 1774, p. 350.]]
En allemand, le terme ''Tonnetz'' (se prononce « tône-netz ») signifie « réseau tonal ». C'est une représentation graphique des notes qui a été imaginée par [[w:Leonhard Euler|Leonhard Euler]] en 1739.
Cette représentation graphique peut aider à la mémorisation de certains concepts de l'harmonie. Cependant, son application est très limitée : elle ne concerne que l'intonation juste d'une part, et que les accords parfait des tonalités majeures et mineures naturelles d'autre part. La représentation contenant les douze notes de la musique savante occidentale, on peut bien sûr représenter d'autres objets, comme les accords de septième ou les accords diminués, mais la représentation graphique est alors compliquée et perd son intérêt pédagogique.
On part d'une note, par exemple le ''do''. Si on progresse vers la droite, on monte d'une quinte juste, donc ''sol'' ; vers la gauche, on descend d'une quinte juste, donc ''fa''. Si on va vers le bas, on monte d'une tierce majeure, donc ''mi'' ; si on va vers le haut, on descend d'une tierce majeure, donc ''la''♭ ou ''sol''♯
fa — do — sol — ré
| | | |
la — mi — si — fa♯
| | | |
do♯ — sol♯ — ré♯ — si♭
La figure forme donc un filet, un réseau. On voit que ce réseau « boucle » : si on descend depuis le ''do''♯, on monte d'une tierce majeure, on obtient un ''mi''♯ qui est l'enharmonique du ''fa'' qui est en haut de la colonne. Si on va vers la droite à partir du ''ré'', on obtient le ''la'' qui est au début de la ligne suivante.
Si on ajoute des diagonales allant vers la droite et le haut « / », on met en évidence des tierces mineures : ''la'' - ''do'', ''mi'' - ''sol'', ''si'' - ''ré'', ''do''♯ - ''mi''…
fa — do — sol — ré
| / | / | / |
la — mi — si — fa♯
| / | / | / |
do♯ — sol♯ — ré♯ — si♭
Donc les liens représentent :
* | : tierce majeure ;
* — : quinte juste ;
* / : tierce mineure.
[[Fichier:Tonnetz carre accords fr.svg|thumb|Tonnetz avec les accords parfaits. Les notes sont en notation italienne et les accords en notation jazz.]]
On met ainsi en évidence des triangles dont un côté est une quinte juste, un côté une tierce majeure et un côté une tierce mineure ; c'est-à-dire que les notes aux sommets du triangle forment un accord parfait majeur (par exemple ''do'' - ''mi'' - ''sol'') :
<div style="font-family:courier; background-color:#fafafa">
fa — '''do — sol''' — ré<br />
| / '''| /''' | / |<br />
la — '''mi''' — si — fa♯<br />
| / | / | / |<br />
do♯ — sol♯ — ré♯ — si♭
</div>
ou un accord parfait mineur (''la'' - ''do'' - ''mi'').
<div style="font-family:courier; background-color:#fafafa">
fa — '''do''' — sol — ré<br />
| '''/ |''' / | / |<br />
'''la — mi''' — si — fa♯<br />
| / | / | / |<br />
do♯ — sol♯ — ré♯ — si♭
</div>
Un triangle représente donc un accord, et un sommet représente une note. Si on passe d'un triangle à un triangle voisin, alors on passe d'un accord à un autre accord, les deux accords ayant deux notes en commun. Ceci illustre la notion de « plus court chemin » en harmonie : si on passe d'un accord à un autre en gardant un côté commun, alors on a un mouvement conjoint sur une seule des trois voix.
Par rapport à l'harmonie fonctionnelle : les accords sont contigus à leur fonction, par exemple en ''do'' majeur :
* fonction de tonique ({{Times New Roman|I}}) : C, A– et E– sont contigus ;
* fonction de sous-dominante ({{Times New Roman|IV}}) : F et D– sont contigus ;
* fonction de dominante ({{Times New Roman|V}}) : G et B<sup>o</sup> sont contigus.
On notera que les triangles d'un schéma ''tonnetz'' ne représentent que des accords parfaits. Pour représenter un accord de quinte diminuée (''si'' - ''ré'' - ''fa'') ou les accords de septième, en particulier l'accord de septième de dominante, il faut étendre le ''tonnetz'' et l'on obtient des figures différentes. Par ailleurs, il est adapté à ce que l'on appelle « l'intonation juste », puisque tous les intervalles sont idéaux.
[[Fichier:Tonnetz carre accords etendu fr.svg|vignette|Tonnetz étendu.]]
[[Fichier:Tonnetz carre do majeur accords fr.svg|vignette|Tonnetz de la tonalité de ''do'' majeur. La représentation de l'accord de quinte diminuée sur ''si'' (B<sup>o</sup>) est une ligne et non un triangle.]]
[[Fichier:Tonnetz carre do mineur accords fr.svg|vignette|Tonnetz des tonalités de ''do'' mineur naturel (haut) et ''do'' mineur harmonique (bas).]]
Si l'on étend un peu le réseau :
ré♭ — la♭ — mi♭ — si♭ — fa
| / | / | / | / |
fa — do — sol — ré — la
| / | / | / | / |
la — mi — si — fa♯ — do♯
| / | / | / | / |
do♯ — sol♯ — ré♯ — la♯ — mi♯
| / | / | / | / |
mi♯ — do — sol — ré — la
on peut donc trouver des chemins permettant de représenter les accords de septième de dominante (par exemple en ''do'' majeur, G<sup>7</sup>)
fa
/
sol — ré
| /
si
et des accords de quinte diminuée (en ''do'' majeur : B<sup>o</sup>)
fa
/
ré
/
si
Une gamme majeure ou mineure naturelle peut se représenter par un trapèze rectangle : ''do'' majeur
fa — do — sol — ré
| /
la — mi — si
et ''do'' mineur
la♭ — mi♭ — si♭
/ |
fa — do — sol — ré
En revanche, la représentation d'une tonalité nécessite d'étendre le réseau afin de pouvoir faire figurer tous les accords, deux notes sont représentées deux fois. La représentation des tonalités mineures harmoniques prend une forme biscornue, ce qui nuit à l'intérêt pédagogique de la représentation.
[[Fichier:Neo-Riemannian Tonnetz.svg|vignette|upright=2|Tonnetz avec des triangles équilatéraux.]]
On peut réorganiser le schéma en décalant les lignes, afin d'avoir des triangles équilatéraux. Sur la figure ci-contre (en notation anglo-saxonne) :
* si on monte en allant vers la droite « / », on a une tierce mineure ;
* si on descend en allant vers la droite « \ », on a une tierce majeure ;
* les liens horizontaux « — » représentent toujours des quintes justes
* les triangles pointe en haut sont des accords parfaits mineurs ;
* les triangles pointe en bas sont des accords parfaits majeurs.
On a alors les accords de septième de dominante
F
/
G — D
\ /
B
et de quinte diminuée
F
/
D
/
B
les tonalités majeures
F — C — G — D
\ /
A — E — B
et les tonalités mineures naturelles
A♭ — E♭ — B♭
/ \
F — C — G — D
== Notes et références ==
{{références}}
== Voir aussi ==
=== Liens externes ===
{{wikipédia|Consonance (harmonie tonale)}}
{{wikipédia|Disposition de l'accord}}
{{wikisource|Petit Manuel d’harmonie}}
* {{lien web
| url = https://www.apprendrelesolfege.com/chiffrage-d-accords
| titre = Chiffrage d'accords (classique)
| site = Apprendrelesolfege.com
| consulté le = 2020-12-03
}}
* {{lien web
| url = https://www.coursd-harmonie.fr/introduction/introduction2_le_chiffrage_d_accords.php
| titre = Introduction II : Le chiffrage d'accords
| site = Cours d'harmonie.fr
| consulté le = 2021-12-14
}}
* {{lien web
| url=https://www.coursd-harmonie.fr/
| titre = Cours d'harmonie en ligne
| auteur = Jean-Baptiste Voinet
| site=coursd-harmonie.fr
| consulté le = 2021-12-20
}}
* {{lien web
| url=http://e-harmonie.e-monsite.com/
| titre = Cours d'harmonie classique en ligne
| auteur = Olivier Miquel
| site=e-harmonie
| consulté le = 2021-12-24
}}
* {{lien web
| url=https://fr.audiofanzine.com/theorie-musicale/editorial/dossiers/les-gammes-et-les-modes.html
| titre = Les bases de l’harmonie
| site = AudioFanzine
| date = 2013-07-23
| consulté le = 2024-01-12
}}
----
''[[../Mélodie|Mélodie]]'' < [[../|↑]] > ''[[../Représentation musicale|Représentation musicale]]''
[[Catégorie:Formation musicale (livre)|Harmonie]]
7jgraqwvf4xejabnj0m40vwbp1d4uam
745868
745867
2025-07-03T11:48:16Z
Cdang
1202
/* Harmonisation par des accords de septième */
745868
wikitext
text/x-wiki
{{Bases de solfège}}
<span style="font-size:25px;">6. Harmonie</span>
L'harmonie désigne les notes jouées en même temps, soit plusieurs instruments jouant chacun une note, soit un instrument jouant un accord (instrument dit polyphonique).
== Première approche ==
L'exemple le plus simple d'harmonie est sans doute la chanson en canon : c'est un chant polyphonique, c'est-à-dire à plusieurs voix, chaque voix chantant la même chose en décalé. Prenons par exemple ''Vent frais, vent du matin'' (la version originale est ''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609) :
[[Fichier:Vent frais vent du matin.svg|class=transparent|center|Partition de ''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]]
[[Fichier:Vent frais vent du matin.midi|vignette|''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]]
nous voyons que les voix se superposent de manière « harmonieuse ». Les notes de chaque voix se correspondent point par point (avec un retard), c'est donc un type d'harmonie polyphonique appelé « contrepoint ».
Considérons la première note de la mesure 6 pour chaque voix. Nous avons la superposition des notes ''ré''-''fa''-''la'' (du grave vers l'aigu) ; la superposition de notes jouées ou chantées ensembles s'appelle un accord. Cet accord ''ré''-''fa''-''la'' porte le nom « d'accord parfait de ''ré'' mineur » :
* « ''ré'' » car la note fondamentale est un ''ré'' ;
* « parfait » car il est l'association d'une tierce, ''ré''-''fa'', et d'une quinte juste, ''ré''-''la'' ;
* « mineur » car le premier intervalle, ''ré''-''fa'', est une tierce mineure.
Considérons maintenant un chant accompagné au piano. La piano peut jouer plusieurs notes en même temps, il peut jouer des accords.
[[Fichier:Au clair de le lune chant et piano.svg|class=transparent|center|Deux premières mesure d’Au clair de la lune.]]
[[Fichier:Au clair de le lune chant et piano.midi|vignette|Deux premières mesure d’Au clair de la lune.]]
L'accord, les notes à jouer simultanément, sont écrites « en colonne ». Lorsqu'on les énonce, on les lit de bas en haut mais le pianiste les joue en pressant les touches du clavier en même temps, de manière « plaquée ».
Le premier accord est composé des notes ''do''-''mi''-''sol'' ; il est appelé « accord parfait de ''do'' majeur » car la note fondamentale est ''do'', qu'il est l'association d'une tierce et d'une quinte juste et que le premier intervalle, ''do''-''mi'', est une tierce majeure.
== Consonance et dissonance ==
Les notions de consonance et de dissonance sont culturelles et changent selon l'époque. Nous pouvons néanmoins noter que :
* l'accord de seconde, et son renversement la septième, créent des battements, les notes « frottent », c'est un intervalle harmonique dissonant ; mais dans le cas de la septième, comme les notes sont éloignées, le frottement est moins perceptible ;
* les accords de tierce, quarte et quinte sonnent agréablement à l'oreille, ils sont consonants.
Dans la musique savante européenne, au début au du Moyen-Âge, seuls les accords de quarte et de quinte étaient considérés comme consonants, d'où leur qualification de « juste ». La tierce, et son renversement la sixte, étaient perçues comme dissonantes.
L'harmonie joue avec les consonances et les dissonances. Dans un premier temps, les harmonies dissonantes sont utilisées pour créer des tensions qui sont ensuite résolues, on utilise des successions « consonant-dissonant-consonant ». À force d'entendre des intervalles considérés comme dissonants, l'oreille s'habitue et certains finissent par être considérés comme consonants ; c'est ce qui est arrivé à la tierce et à la sixte à la fin du Moyen Âge avec le contrepoint.
Il faut ici aborder la notion d'harmonique des notes.
[[File:Harmoniques de do.svg|thumb|Les six premières harmoniques de ''do''.]]
Lorsque l'on joue une note, on entend d'autres notes plus aigües et plus faibles ; la note jouée est appelée la « fondamentale » et les notes plus aigües et plus faibles sont les « harmoniques ». C'est cette accumulation d'harmoniques qui donne la couleur au son, son timbre, qui fait qu'un piano ne sonne pas comme un violon. Par exemple, si l'on joue un ''do''<sup>1</sup><ref>Pour la notation des octaves, voir ''[[../Représentation_musicale#Désignation_des_octaves|Représentation musicale > Désignation des octaves]]''.</ref> (fondamentale), on entend le ''do''<sup>2</sup> (une octave plus aigu), puis un ''sol''<sup>2</sup>, puis encore un ''do''<sup>3</sup> plus aigu, puis un ''mi''<sup>3</sup>, puis encore un ''sol''<sup>3</sup>, puis un ''si''♭<sup>3</sup>…
Ainsi, puisque lorsque l'on joue un ''do'' on entend aussi un ''sol'' très léger, alors jouer un ''do'' et un ''sol'' simultanément n'est pas choquant. De même pour ''do'' et ''mi''. De là vient la notion de consonance.
Le statut du ''si''♭ est plus ambigu. Il fait partie des harmoniques qui sonnent naturellement, mais il forme une seconde descendante avec le ''do'', intervalle dissonant. Par ailleurs, on remarque que le ''si''♭ ne fait pas partie de la gamme de ''do'' majeur, contrairement au ''sol'' et au ''mi''.
Pour le jeu sur les dissonances, on peut écouter par exemple la ''Toccata'' en ''ré'' mineur, op. 11 de Sergueï Prokofiev (1912).
: {{lien web |url=https://www.youtube.com/watch?v=AVpnr8dI_50 |titre=Yuja Wang Prokofiev Toccata |site=YouTube |date=2019-02-26 |consulté le=2021-12-19}}
== Contrepoint ==
Dans le chant grégorien, la notion d'accord n'existe pas. L'harmonie provient de la superposition de plusieurs mélodies, notamment dans ce que l'on appelle le « contrepoint ».
Le terme provient du latin ''« punctum contra punctum »'', littéralement « point par point », et désigne le fait que les notes de chaque voix se correspondent.
L'exemple le plus connu de contrepoint est le canon, comme par exemple ''Frère Jacques'' : chaque note d'un couplet correspond à une note du couplet précédent.
Certains morceaux sont bâtis sur une écriture « en miroir » : l'ordre des notes est inversé entre les deux voix, ou bien les intervalles sont inversés (« mouvement contraire » : une tierce montante sur une voix correspond à une tierce descendante sur l'autre).
On peut également citer le « mouvement oblique » (une des voix, le bourdon, chante toujours la même note) et le mouvement parallèle (les deux voix chantent le même air mais transposé, l'une est plus aiguë que l'autre).
Nous reproduisons ci-dessous le début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.
[[Fichier:Haendel Sonate en trio re mineur debut canon.svg | vignette | center | upright=2 | Début du second ''Allergo'' de la sonate en trio en ''ré'' mineur de Haendel.]]
[[Fichier:Haendel Sonate en trio re mineur debut.midi | vignette | Début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.]]
Nous avons mis en évidence la construction en canon avec des encadrés de couleur : sur les quatre premières mesures, nous voyons trois thèmes repris alternativement par une voix et par l'autre. Ce type de procédé est très courant dans la musique baroque.
Les procédés du contrepoint s'appliquent également à la danse :
* unisson : les danseurs et danseuses font les mêmes gestes en même temps ;
* répétition : le fait de répéter une série de gestes, une « phrase dansante » ;
* canon : les gestes sont faits avec un décalage régulier d'un danseur ou d'une danseuse à l'autre ;
* cascade : forme de canon dans laquelle le décalage est très petit ;
* contraste : deux danseur·euses, ou deux groupes, ont des gestuelles très différentes ;
* accumulation : la gestuelle se complexifie par l'ajout d'éléments au fur et à mesure ; ou bien le nombre de danseur·euses augmente ;
* dialogue : les gestes de danseur·euses ou de groupes se répondent ;
* contre-point : la gestuelle d'un ou une danseuse se superpose à la gestuelle d'un groupe ;
* lâcher-rattraper : les danseurs et danseuses alternent danse à l'unisson et gestuelles indépendantes.
: {{lien web
| url=https://www.youtube.com/watch?v=wgblAOzedFc
| titre=Les procédés de composition en danse
| auteur= Doisneau Sport TV
| site=YouTube
| date=2020-03-16 | consulté le=2021-01-21
}}
{{...}}
== Les accords en général ==
Initialement, on a des chants polyphoniques, des voix qui chantent chacune une mélodie, les mélodies se mêlant. On remarque que certaines superpositions de notes sonnent de manière plus ou moins agréables, consonantes ou dissonantes. On en vient alors à associer ces notes, c'est-à-dire à considérer dès le départ la superposition de ces notes et non pas la rencontre de ces notes au gré des mélodies. Ces groupes de notes superposées forment les accords. En Europe, cette notion apparaît vers le {{pc|xiv}}<sup>e</sup> siècle avec notamment la ''[[wikipedia:fr:Messe de Notre Dame|Messe de Notre Dame]]'' de Guillaume de Machaut (vers 1360-1365). La notion « d'accord parfait » est consacrée par [[wikipedia:fr:Jean-Philippe Rameau|Jean-Philippe Rameau]] dans son ''Traité de l'harmonie réduite à ses principes naturels'', publié en 1722.
=== Qu'est-ce qu'un accord ? ===
Un accord est un ensemble d'au minimum trois notes jouées en même temps. « Jouées » signifie qu'il faut qu'à un moment donné, elles sonnent en même temps, mais le début ou la fin des notes peut être à des instants différents.
Considérons que l'on joue les notes ''do'', ''mi'' et ''sol'' en même temps. Cet accord s'appelle « accord de ''do'' majeur ». En musique classique, on lui adjoint l'adjectif « parfait » : « accord parfait de ''do'' majeur ».
Nous représentons ci-dessous trois manière de faire l'accord : avec trois instruments jouant chacun une note :
[[Fichier:Do majeur trois portees.svg|class=transparent|center|Accord de ''do'' majeur avec trois instruments différents.]]
Avec un seul instrument jouant simultanément les trois notes :
[[Fichier:Chord C.svg|class=transparent|center|Accord de ''do'' majeur joué par un seul instrument.]]
L'accord tel qu'il est joué habituellement par une guitare d'accompagnement :
[[Fichier:Do majeur guitare.svg|class=transparent|center|Accord de ''do'' majeur à la guitare.]]
Pour ce dernier, nous représentons le diagramme indiquant la position des doigts sur le manche au dessus de la portée et la tablature en dessous. Ici, c'est au total six notes qui sont jouées : ''mi'' grave, ''do'' médium, ''mi'' médium, ''sol'' médium, ''do'' aigu, ''mi'' aigu. Mais il s'agit bien des trois notes ''do'', ''mi'' et ''sol'' jouées à des octaves différentes. Nous remarquons également que la note de basse (la note la plus grave), ''mi'', est différente de la note fondamentale (celle qui donne le nom à l'accord), ''do'' ; l'accord est dit « renversé » (voir plus loin).
=== Comment joue-t-on un accord ? ===
Les notes ne sont pas forcément jouées en même temps ; elles peuvent être « égrainées », jouée successivement, ce que l'on appelle un arpège. La partition ci-dessous montre six manières différentes de jouer un accord de ''la'' mineur à la guitare, plaqué puis arpégé.
[[Fichier:La mineur differentes executions.svg|class=transparent|center|Différentes exécution de l'accord de do majeur à la guitare.]]
[[Fichier:La mineur differentes executions midi.midi|vignette|Différentes exécution de l'accord de la mineur à la guitare.]]
Vous pouvez écouter l'exécution de cette partition avec le lecteur ci-contre.
Seuls les instruments polyphoniques peuvent jouer les accords plaqués : instruments à clavier (clavecin, orgue, piano, accordéon), les instruments à plusieurs cordes pincées (harpe, guitare ; violon, alto, violoncelle et contrebasse joués en pizzicati). Les instruments à corde frottés de la famille du violon peuvent jouer des notes par deux à l'archet mais pas plus du fait de la forme bombée du chevalet ; cependant, un mouvement rapide permet de jouer les quatre cordes de manière très rapprochée. Les instruments à percussion de type xylophone ou le tympanon permettent de jouer jusqu'à quatre notes simultanément en tenant deux baguettes (mailloches, maillets) par main.
Tous les instruments peuvent jouer des arpèges même si, dans le cas des instruments monodiques, les notes ne continuent pas à sonner lorsque l'on passe à la note suivante.
L'arpège peut être joué par l'instrument de basse (basson, violoncelle, contrebasse, guitare basse, pédalier de l'orgue…), notamment dans le cas d'une basse continue ou d'une ''{{lang|en|walking bass}}'' (« basse marchante » : la basse joue des noires, donnant ainsi l'impression qu'elle marche).
En jazz, et spécifiquement au piano, on a recours au ''{{lang|en|voicing}}'' : on choisit la manière dont on organise les notes pour donner une couleur spécifique, ou bien pour créer une mélodie en enchaînant les accords. Il est fréquent de ne pas jouer toutes les notes : si on n'en garde que deux, ce sont la tierce et la septième, car ce sont celles qui caractérisent l'accord (selon que la tierce est mineure ou majeure, que la septième est majeure ou mineure), et la fondamentale est en général jouée par la contrebasse ou guitare basse.
{{clear}}
=== Classes d'accord ===
[[Fichier:Intervalles harmoniques accords classes.svg|vignette|upright=1.5|Intervalles harmoniques dans les accords classés de trois, quatre et cinq notes.]]
Un accord composé d'empilement de tierces est appelé « accord classé ». En musique tonale, c'est-à-dire la musique fondée sur les gammes majeures ou mineures (cas majoritaire en musique classique), on distingue trois classes d'accords :
* les accords de trois notes, ou triades, ou accords de quinte ;
* les accords de quatre notes, ou accords de septième ;
* les accords de cinq notes, ou accords de neuvième.
En empilant des tierces, si l'on part de la note fondamentale, on a donc de intervalles de tierce, quinte, septième et neuvième.
En musique tonale, les accords avec d'autres intervalles (hors renversement, voir ci-après), typiquement seconde, quarte ou sixte, sont considérés comme des transitions entre deux accords classés. Ils sont appelés, selon leur utilisation, « accords à retard » (en anglais : ''{{lang|en|suspended chord}}'', accord suspendu) ou « appoggiature » (note « appuyée », étrangère à l'harmonie). Voir aussi plus loin la notion de note étrangère.
=== Renversements d'accords ===
[[File:Accord do majeur renversements.svg|thumb|Accord parfait de do majeur et ses renversements.]]
[[Fichier:Progression dominante renverse parfait do majeur.svg|vignette|upright=0.6|Progression accord de dominante renversé → accord parfait en ''do'' majeur.]]
Un accord classé est donc un empilement de tierces. Si l'on change l'ordre des notes, on a toujours le même accord mais il est fait avec d'autres intervalles harmoniques. Par exemple, l'accord parfait de ''do'' majeur dans son état fondamental, c'est-à-dire non renversé, s'écrit ''do'' - ''mi'' - ''sol''. Sa note fondamentale, ''do'', est aussi se note de basse.
Si maintenant on prend le ''do'' de l'octave supérieure, l'accord devient ''mi - sol - do'' ; c'est l'empilement d'une tierce ''(mi - sol)'' et d'une quarte ''(sol - do)'', soit la superposition d'une tierce ''(mi - sol)'' et d'une sixième ''(mi - do)''. C'est le premier renversement de l'accord parfait de ''do'' majeur ; la fondamentale est toujours ''do'' mais la basse est ''mi''. Le second renversement est ''sol - do - mi''.
L'utilisation de renversement peut faciliter l'exécution de la progression d'accord. Par exemple, en tonalité ''do'' majeur, si l'on veut passer de l'accord de dominante ''sol - si - ré'' à l'accord parfait ''do - mi - sol'', alors on peut utiliser le second renversement de l'accord de dominante : ''ré - sol - si'' → ''do - mi - sol''. Ainsi, la basse descend juste d'un ton ''(ré → do)'' et sur un piano, la main reste globalement dans la même position.
Le renversement d'un accord permet également de respecter certaines règles de l'harmonie classique, notamment éviter que des voix se suivent strictement (« mouvement parallèle »), ce qui aurait un effet de platitude.
De manière générale, la notion de renversement permet deux choses :
* d'enrichir l'œuvre : pour créer une harmonie donnée (c'est-à-dire des sons sonnant bien ensemble), nous avons plus de souplesse, nous pouvons organiser ces notes comme nous le voulons selon les voix ;
* de simplifier l'analyse : quelle que soit la manière dont sont organisées les notes, cela nous ramène à un même accord.
{{citation bloc|Or il, y a plusieurs manières de jouer un accord, selon que l'on aborde par la première note qui le constitue, ''do mi sol'', la deuxième, ''mi sol do'', ou la troisième note, ''sol do mi''. Ce sont les renversements, [que Rameau] va classer en différentes combinaisons d'une seule matrice. Faisant cela, Rameau divise le nombre d'accords [de septième] par quatre. Il simplifie, il structure […].|{{ouvrage|prénom1=André |nom1=Manoukian |titre=Sur les routes de la musique |éditeur=Harper Collins |année=2021 |passage=54 |isbn=979-1-03391201-9}} }}
{{clear}}
[[File:Plusieurs realisation 1er renversement doM.svg|thumb|Plusieurs réalisation du premier renversement de l'accord de ''do'' majeur.]]
Notez que dans la notion de renversement, seule importe en fait la note de basse. Ainsi, les accords ''mi-sol-do'', ''mi-do-sol'', ''mi-do-mi-sol'', ''mi-sol-mi-do''… sont tous une déclinaison du premier renversement de ''do-mi-sol'' et ils seront abrégés de la même manière (''mi''<sup>6</sup> en musique classique ou C/E en musique populaire et jazz, voir plus bas).
{{clear}}
== Notation des accords de trois notes ==
Les accords de trois notes sont appelés « accords de quinte » en classique, et « triades » en jazz.
[[Fichier:Progression dominante renverse parfait do majeur chiffrage.svg|vignette|upright=0.7|Chiffrage du second renversement d'un accord de ''sol'' majeur et d'un accord de ''do'' majeur : notation en musique populaire et jazz (haut) et notation de basse chiffrée (bas).]]
Les accords sont construits de manière systématique. Nous pouvons donc les représenter de manière simplifiée. Cette notation simplifiée des accords est appelée « chiffrage ».
Reprenons la progression d'accords ci-dessus : « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » dans la tonalité de ''do'' majeur. On utilise en général trois notations différentes :
* en musique populaire, jazz, rock… un accord est désigné par sa note fondamentale ; ici donc, les accords sont notés « ''sol'' - ''do'' » ou, en notation anglo-saxonne, « G - C » ;<br /> comme le premier accord est renversé, on indique la note de basse après une barre, la progression d'accords est donc chiffrée '''« ''sol''/''ré'' - ''do'' »''' ou '''« G/D - C »''' ;<br /> il s'agit ici d'accords composés d'une tierce majeure et d'une quinte juste ; si les accords sont constitués d'intervalles différents, nous ajoutons un symbole après la note : « m » ou « – » si la tierce est mineure, « dim » ou « ° » si la quinte est diminuée ;
* en musique classique, on utilise la notation de « basse chiffrée » (utilisée notamment pour noter la basse continue en musique baroque) : on indique la note de basse sur la portée et on lui adjoint l'intervalle de la fondamentale à la note la plus haute (donc ici respectivement 6 et 5, puisque ''sol''-''si'' est une sixte et ''do''-''sol'' est une quinte), étant sous-entendu que l'on a des empilements de tierce en dessous ; mais dans le cas du premier accord, le premier intervalle n'est pas une tierce, mais une quarte ''(ré''-''sol)'', on note donc '''« ''ré'' <sup>6</sup><sub>4</sub> - ''do'' <sup>5</sup> »'''<ref>quand on ne dispose pas de la notation en supérieur (exposant) et inférieur (indice), on utilise parfois une notation sous forme de fraction : ''sol'' 6/4 et ''do'' 5/.</ref> ;
* lorsque l'on fait l'analyse d'un morceau, on s'attache à identifier la note fondamentale de l'accord (qui est différente de la basse dans le cas d'un renversement) ; on indique alors le degré de la fondamentale : '''« {{Times New Roman|V<sup>6</sup><sub>4</sub> - I<sup>5</sup>}} »'''.
La notation de basse chiffrée permet de construire l'accord à la volée :
* on joue la note indiquée (basse) ;
* s'il n'y a pas de 2 ni de 4, on lui ajoute la tierce ;
* on ajoute les intervalles indiqués par le chiffrage.
La notation de musique jazz oblige à connaître la composition des différents accords, mais une fois que ceux-ci sont acquis, il n'y a pas besoin de reconstruire l'accord.
La notation de basse chiffrée avec les chiffres romains n'est pas utilisée pour jouer, mais uniquement pour analyser ; Sur les partitions avec basse chiffrée, il y a simplement les chiffrages indiqués au-dessus de la partie de basse. Le chiffrage avec le degré en chiffres romains présente l'avantage d'être indépendant de la tonalité et donc de se concentrer sur la fonction de l'accord au sein de la tonalité. Par exemple, ci-dessous, nous pouvons parler de la progression d'accords « {{Times New Roman|V - I}} » de manière générale, cette notation étant valable quelle que soit la tonalité.
[[File:Progression dominante renverse parfait do majeur chiffrage basse continue.svg|thumb|Chiffrage en notation basse chiffrée de la progression d'accords « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » en do majeur.]]
{{note|En notation de base continue avec fondamentale en chiffres romains, la fondamentale est toujours indiquée ''sous'' la portée de la partie de basse. Les intervalles sont indiqués au-dessus de la portée de la partie de basse ; lorsque l'on fait une analyse, on peut ayssi les indiquer à côté du degré en chiffres romains, donc sous la portée de la basse.}}
{{note|En notation rock, le 5 en exposant indique un accord incomplet avec uniquement la fondamentale et la quinte, un accord sans tierce appelé « accord de puissance » ou ''{{lang|en|power chord}}''. Par exemple, C<sup>5</sup> est l'accord ''do-sol''.}}
{{clear}}
[[Fichier:Accords parfait do majeur basse chiffree fondamental et renverse.svg|vignette|upright=2.5|Chiffrage de l'accord parfait de ''do'' majeur en basse chiffrée, à l'état fondamental et ses renversements.]]
Concernant les accords parfaits en notation de basse chiffrée :
* un accord parfait à l'état fondamental est chiffré « <sup>5</sup> » ; on l'appelle « accord de quinte » ;
* le premier renversement est chiffré « <sup>6</sup> » (la tierce est implicite) ; on l'appelle « accord de sixte » ;
* le second renversement est noté « <sup>6</sup><sub>4</sub> » ; on l'appelle « accord de sixte et de quarte » (ou bien « de quarte et de sixte »).
Par exemple, pour l'accord parfait de ''do'' majeur :
* l'état fondamental ''do''-''mi''-''sol'' est noté ''do''<sup>5</sup> ;
* le premier renversement ''mi''-''sol''-''do'' est noté ''mi''<sup>6</sup> ;
* le second renversement ''sol''-''do''-''mi'' est noté ''sol''<sup>6</sup><sub>4</sub>.
Il y a une exception : l'accord construit sur la sensible (7{{e}} degré) contient une quinte diminuée et non une quinte juste. Le chiffrage est donc différent :
* l'état fondamental ''si''-''ré''-''fa'' est noté ''si''<sup><s>5</s></sup> (cinq barré), « accord de quinte diminuée » ;
* le premier renversement ''ré''-''fa''-''si'' est noté ''ré''<sup>+6</sup><sub>3</sub>, « accord de sixte sensible et tierce » ;
* le second renversement ''fa''-''si''-''ré'' est noté ''fa''<sup>6</sup><sub>+4</sub>, « accord de sixte et quarte sensible ».
Par ailleurs, on ne considère pas qu'il est fondé sur la sensible, mais sur la dominante ; on met donc des guillemets autour du degré, « “V” ». Donc selon l'état, le chiffrage est “V”<sup><s>5</s></sup>, “V”<sup>+6</sup><sub>3</sub> ou “V”<sup>6</sup><sub>+4</sub>.
En notation jazz, on ajoute « dim », « <sup>o</sup> » ou bien « <sup>♭5</sup> » au chiffrage, ici : B dim, B<sup>o</sup> ou B<sup>♭5</sup> pour l'état fondamental. Pour les renversements : B dim/D et B dim/F ; ou bien B<sup>o</sup>/D et B<sup>o</sup>/F ; ou bien B<sup>♭5</sup>/D et B<sup>♭5</sup>/F.
{{clear}}
[[Fichier:Accords basse chiffree basse do fondamental et renverses.svg|vignette|upright=2|Basse chiffrée : accords de quinte, de sixte et de sixte et de quarte ayant pour basse ''do''.]]
Et concernant les accords ayant pour basse ''do'' en tonalité de ''do'' majeur :
* l'accord ''do''<sup>5</sup> est un accord à l'état fondamental, c'est donc l'accord ''do''-''mi''-''sol'' (sa fondamentale est ''do'') ;
* l'accord ''do''<sup>6</sup> est le premier renversement d'un accord, c'est donc l'accord ''do''-''mi''-''la'' (sa fondamentale est ''la'') ;
* l'accord ''do''<sup>6</sup><sub>4</sub> est le second renversement d'un accord, c'est donc l'accord ''do''-''fa''-''la'' (sa fondamentale est ''fa'').
{{clear}}
== Notes étrangères ==
La musique européenne s'appuie essentiellement sur des accords parfaits, c'est-à-dire fondés sur une tierce majeure ou mineure, et une quinte juste. Il arrive fréquemment qu'un accord ne soit pas un accord parfait. Les notes qui font partie de l'accord parfait sont appelées « notes naturelles » et la note qui n'en fait pas partie est appelée « note étrangère ».
Il existe plusieurs types de notes étrangères :
* anticipation : la note étrangère est une note naturelle de l'accord suivant ;
* appogiature : note d'ornementation qui se résout par mouvement conjoint, c'est-à-dire qu'elle est suivie par une note située juste au-dessus ou en dessous (seconde ascendante ou descendante) qui est, elle, une note naturelle ;
* broderie : on part d'une note naturelle, on monte ou on descend d'une seconde, puis on revient sur la note naturelle ;
* double broderie : on part d'une note naturelle, on joue la note du dessus puis la note du dessous avant de revenir à la note naturelle ; ou bien on joue la note du dessous puis la note du dessus ;
* échappée : note étrangère n'appartenant à aucune des autres catégories ;
* note de passage : mouvement conjoint allant d'une note naturelle d'un accord à une note naturelle de l'accord suivant ;
* pédale : la note de basse reste la même pendant plusieurs accords successifs ;
* retard : la note étrangère est une note naturelle de l'accord précédent.
Les notes étrangères ne sont pas chiffrées.
[[File:Notes etrangeres accords.svg|center|Différents types de notes étrangères.]]
{{note|Les anglophones distinguent deux types de retard : la ''{{lang|en|suspension}}'' est résolue vers le haut (le mouvement est ascendant), le ''{{lang|en|retardation}}'' est résolu vers le bas (le mouvement est descendant).}}
== Principaux accords ==
Les trois principaux accords sont :
* l'accord parfait majeur : il est construit sur les degrés {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (médiante) et {{Times New Roman|V}} (dominante) d'une gamme majeure ; il est noté {{Times New Roman|I}}<sup>5</sup>, {{Times New Roman|IV}}<sup>5</sup>, {{Times New Roman|V}}<sup>5</sup> ;
* l'accord parfait mineur : il est construit sur les degrés {{Times New Roman|I}} (tonique) et {{Times New Roman|IV}} (sous-tonique) d'une gamme mineure harmonique ; il est également noté {{Times New Roman|I}}<sup>5</sup> et {{Times New Roman|IV}}<sup>5</sup>, les anglo-saxons le notent {{Times New Roman|i}}<sup>5</sup> et {{Times New Roman|iv}}<sup>5</sup> (la minuscule indiquant le caractère mineur) ;
* l'accord de septième de dominante : il est construit sur le degré {{Times New Roman|V}} (dominante) d'une gamme majeure ou mineure harmonique ; il est noté {{Times New Roman|V}}<sup>7</sup><sub>+</sub>.
On peut trouver ces trois accords sur d'autres degrés, et il existe d'autre types d'accords. Nous verrons cela plus loin.
{| class="wikitable"
|+ Constitution des principaux accords — Dénomination classique
|-
! scope="col" | Accord
! scope="col" | 1<sup>er</sup> intervalle
! scope="col" | 2<sup>e</sup> intervalle
! scope="col" | 3<sup>e</sup> intervalle
|-
! scope="row" | Accord parfait majeur
| tierce majeure (3M) || quinte juste (5J) || —
|-
! scope="row" | Accord parfait mineur
| tierce mineure (3m) || quinte juste (5J) || —
|-
! scope="row" | Accord de septième de dominante
| tierce majeure (3M) || quinte juste (5J) || septième mineure (7m)
|}
{| class="wikitable"
|+ Constitution des principaux accords — Dénomination jazz
|-
! scope="col" | Accord
! scope="col" | 1<sup>er</sup> intervalle
! scope="col" | 2<sup>e</sup> intervalle
! scope="col" | 3<sup>e</sup> intervalle
|-
! scope="row" | Triade majeure
| tierce majeure (3M) || quinte juste (5J) || —
|-
! scope="row" | Triade mineure
| tierce mineure (3m) || quinte juste (5J) || —
|-
! scope="row" | Accord de septième
| tierce majeure (3M) || quinte juste (5J) || septième mineure (7m)
|}
{| border="0"
|-
| [[Fichier:Accord do majeur arpege puis plaque.midi | Accord parfait de ''do'' majeur (C).]] || [[Fichier:Accord do mineur arpege puis plaque.midi | Accord parfait de ''do'' mineur (Cm).]] || [[Fichier:Accord do septieme arpege puis plaque.midi | Accord de septième de dominante de ''fa'' majeur (C<sup>7</sup>).]]
|-
| Accord parfait<br /> de ''do'' majeur (C). || Accord parfait<br /> de ''do'' mineur (Cm). || Accord de septième de dominante<br /> de ''fa'' majeur (C<sup>7</sup>).
|}
'''Rappel :'''
* la tierce mineure est composée d'un ton et demi (1 t ½) ;
* la tierce majeur est composée de deux tons (2 t) ;
* la quinte juste a la même altération que la fondamentale, sauf lorsque la fondamentale est ''si'' (la quinte juste est alors ''fa''♯) ;
* la septième mineure est le renversement de la seconde majeure (1 t).
[[File:Renversements accords pft fa maj basse chiffree.svg|thumb|Renversements de l'accord parfait de ''fa'' majeur, et la notation de basse chiffrée.]]
[[File:Renversements accord sept de dom fa maj basse chiffree.svg|thumb|Renversements de l'accord de septième de dominante de ''fa'' majeur, et la notation de basse chiffrée.]]
{| class="wikitable"
|+ Notation des principaux accords en musique classique
|-
! scope="col" | Accord
! scope="col" | État<br /> fondamental
! scope="col" | Premier<br /> renversement
! scope="col" | Deuxième<br /> renversement
! scope="col" | Troisième<br /> renversement
|-
! scope="row" | Accord parfait
| {{Times New Roman|I<sup>5</sup>}}<br/> acc. de quinte || {{Times New Roman|I<sup>6</sup>}}<br :> acc. de sixte || {{Times New Roman|I<sup>6</sup><sub>4</sub>}}<br /> acc. de quarte et de sixte || —
|-
! scope="row" | Accord de septième<br /> de dominante
| {{Times New Roman|V<sup>7</sup><sub>+</sub>}}<br /> acc.de septième de dominante || {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}<br />acc. de sixte et quinte diminuée || {{Times New Roman|V<sup>+6</sup>}}<br />acc. de sixte sensible || {{Times New Roman|V<sup>+4</sup>}}<br />acc. de quarte sensible<br />acc. de triton
|}
{| class="wikitable"
|+ Notation des principaux accords en jazz
|-
! scope="col" | Accord
! scope="col" | Chiffrage
! scope="col" | Renversements
|-
! scope="row" | Triade majeure
| X
| rowspan="3" | Les renversements se notent en mettant la basse après une barre de fraction, par exemple pour la triade de ''do'' majeur :
* état fondamental : C ;
* premier renversement : C/E ;
* second renversement : C/G.
|-
! scope="row" | Triade mineure
| Xm, X–
|-
! scope="row" | Septième
| X<sup>7</sup>
|}
{{clear}}
Dans le cas d'un accord de septième de dominante, le nom de l'accord change selon que l'on est en musique classique ou en jazz : en musique classique, on donne le nom de la tonalité alors qu'en jazz, on donne le nom de la fondamentale. Ainsi, l'accord appelé « septième de dominante de ''do'' majeur » en musique classique, est appelé « ''sol'' sept » (G<sup>7</sup>) en jazz : la dominante (degré {{Times New Roman|V}}, dominante) de la tonalité de ''do'' majeur est la note ''sol''.
Comment appelle-t-on en musique classique l'accord appelé « ''do'' sept » (C<sup>7</sup>) en jazz ? Les tonalités dont le ''do'' est la dominante sont les tonalités de ''fa'' majeur (''si''♭ à la clef) et de ''fa'' mineur harmonique (''si''♭, ''mi''♭, ''la''♭ et ''ré''♭ à la clef et ''mi''♮ accidentel). Il s'agit donc de l'accord de septième de dominante des tonalités de ''fa'' majeur et ''fa'' mineur harmonique.
{| class="wikitable"
|+ Accords fréquents pour quelques la tonalités majeures
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Accord parfait<br />{{Times New Roman|I<sup>5</sup>}}
! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}}
|-
|''Do'' majeur || || C<br />''do-mi-sol'' || G7<br />''sol-si-ré-fa''
|-
|''Sol'' majeur || ''fa''♯ || G<br />''sol-si-ré'' || D7<br />''ré-fa''♯''-la-do''
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || D<br />''ré-fa''♯''-la'' || A7<br />''la-do''♯''-mi-sol''
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A<br />''la-do''♯''-mi'' || E7<br />''mi-sol''♯''-si-ré''
|-
| ''Fa'' majeur || ''si''♭ || F<br />''fa-la-do'' || C7<br />''do-mi-sol-si''♭
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭<br />''si''♭''-ré-fa'' || F7<br />''fa-la-do-mi''♭
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭<br />''mi''♭''-sol-si''♭ || B♭7<br />''si''♭''-ré-fa-la''♭
|}
{| class="wikitable"
|+ Accords fréquents pour quelques la tonalités mineures harmoniques
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Accord parfait<br />{{Times New Roman|i<sup>5</sup>}}
! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}}
|-
|''La'' mineur<br />harmonique || || Am, A–<br />''la-do-mi'' || E7<br />''mi-sol''♯''-si-ré''
|-
|''Mi'' mineur<br />harmonique || ''fa''♯ || Em, E–<br />''mi-sol-si'' || B7<br />''si-ré''♯''-fa''♯''-la''
|-
|''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || Bm, B–<br />''si-ré-fa''♯ || F♯7<br />''fa''♯''la''♯''-do''♯''-mi''
|-
|''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || F♯m, F♯–<br />''fa''♯''-la-do''♯ || C♯7<br />''do''♯''-mi''♯''-sol''♯''-si''
|-
| ''Ré'' mineur<br />harmonique || ''si''♭ || Dm, D–<br />''ré-fa-la'' || A7<br />''la-do''♯''-mi-sol''
|-
| ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || Gm, G–<br />''sol-si''♭''-ré'' || D7<br />''ré-fa''♯''-la-do''
|-
| ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || Cm, C–<br />''do-mi''♭''-sol'' || G7<br />''sol-si''♮''-ré-fa''
|}
{{clear}}
== Accords sur les degrés d'une gamme ==
=== Harmonisation d'une gamme ===
[[Fichier:Accord trois notes gamme do majeur chiffre.svg|vignette|upright=1.2|Accords de trois note sur la gamme de ''do'' majeur, chiffrés.]]
On peut ainsi construire une triade par degré d'une gamme.
Pour une gamme majeure, les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|V<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|II<sup>5</sup>}}, {{Times New Roman|III<sup>5</sup>}}, {{Times New Roman|VI<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|ii<sup>5</sup>}}, {{Times New Roman|iii<sup>5</sup>}}, {{Times New Roman|vi<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}.
Les accords ont tous une quinte juste à l'exception de l'accord {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} qui a une quinte diminuée, raison pour laquelle le « 5 » est barré. C'est un accord dit « de quinte diminuée ». En jazz, l'accord diminué est noté « dim », « ° », « m<sup>♭5</sup> » ou « <sup>–♭5</sup> ».
Nous avons donc trois types d'accords (dans la notation jazz) : X (triade majeure), Xm (triade mineure) et X° (triade diminuée), la lettre X remplaçant le nom de la note fondamentale.
{{clear}}
[[Fichier:Accord trois notes gamme la mineur chiffre.svg|vignette|upright=1.2|Accords de trois notes sur une gamme de ''la'' mineur harmonique, chiffrés.]]
Pour une gamme mineure harmonique, les accords {{Times New Roman|III<sup>+5</sup>}}, {{Times New Roman|V<sup>♯</sup>}} et {{Times New Roman|VI<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|II<sup><s>5</s></sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|i<sup>5</sup>}}, {{Times New Roman|ii<sup><s>5</s></sup>}}, {{Times New Roman|iv<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}.
Les accords {{Times New Roman|ii<sup><s>5</s></sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} ont une quinte diminuée ; ce sont des accords dits « de quinte diminuée ». L'accord {{Times New Roman|III<sup>+5</sup>}} a une quinte augmentée ; le signe « plus » indique que la note de cinquième, le ''sol'' dièse, est la sensible. En jazz, l'accord est noté « aug » ou « <sup>+</sup> ». Les autres accords ont une quinte juste.
Aux trois accords générés par une gamme majeure (X, Xm et X°), nous voyons ici apparaître un quatrième type d'accord : la triade augmentée X<sup>+</sup>.
Nous remarquons que des gammes ont des accords communs. Par exemple, l'accord {{Times New Roman|ii<sup>5</sup>}} de ''do'' majeur est identique à l'accord {{Times New Roman|iv<sup>5</sup>}} de ''la'' mineur (il s'agit de l'accord Dm).
Quel que soit le mode, les accords construits sur la sensible (accord de quinte diminuée) sont rarement utilisés. S'ils le sont, c'est en tant qu'accord de septième de dominante sans fondamentale (voir ci-après). C'est la raison pour laquelle le chiffrage indique le degré {{Times New Roman|V}} entre guillemets, et non pas le degré {{Times New Roman|VII}} (mais pour des raisons de clarté, nous l'indiquons entre parenthèses au début).
En mode mineur, l'accord de quinte augmentée {{Times New Roman|iii<sup>+5</sup>}} est très peu utilisé (voir plus loin ''[[#Progression_d'accords|Progression d'accords]]''). C'est un accord considéré comme dissonant.
On voit que :
* un accord parfait majeur peut appartenir à cinq gammes différentes ;<br /> par exemple l'accord parfait de ''do'' majeur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> degré de la gamme de ''do'' majeur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' mineur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''mi'' mineur ;
* un accord parfait mineur peut appartenir à cinq gammes différentes ;<br />par exemple l'accord parfait de ''la'' mineur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> de la gamme de ''la'' mineur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''mi'' mineur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|III}}<sup>e</sup> degré de ''fa'' majeur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''do'' majeur ;
* un accord de quinte diminuée peut appartenir à trois gammes différentes ;<br />par exemple, l'accord de quinte diminuée de ''si'' est l'accord construit sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' majeur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''la'' mineur et sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' mineur ;
* un accord de quinte augmentée (à l'état fondamental) ne peut appartenir qu'à une seule gamme ;<br /> par exemple, l'accord de quinte augmentée de ''do'' est l'accord construit sur le {{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur.
{| class="wikitable"
|+ Notation jazz des triades
|-
| rowspan="2" colspan="2" |
! scope="col" colspan="2" | Tierce
|-
! scope="col" | 3m
! scope="col" | 3M
|-
! rowspan="3" | Quinte
! scope="row" | 5d
| Xᵒ, X<sub>m</sub><sup>(♭5)</sup> ||
|-
! scope="row" | 5J
| Xm, X– || X
|-
! scope="row" | 5A
| || X+, X<sup>(♯5)</sup>
|}
=== Harmonisation par des accords de septième ===
[[Fichier:Harmonisation gamme do majeur par septiemes chiffre.svg|vignette|upright=2|Harmonisation de la gamme de do majeur par des accords de septième.]]
Les accords de septième contiennent une dissonance et créent ainsi une tension. Ils sont très utilisés en jazz. Nous avons représenté ci-contre l'harmonisation de la gamme de ''do'' majeur.
La constitution des accords est la suivantes :
* tierce majeure (3M)
** quinte juste (5J)
*** septième mineure (7m) : sur le degré V, c'est l'accord de septième de dominante V<sup>7</sup><sub>+</sub>, noté X<sup>7</sup> (X pour G),
*** septième majeure (7M) : sur les degrés I et IV, appelés « accords de septième majeure » et notés aussi X<sup>maj7</sup> ou X<sup>Δ</sup> (X pour C ou F) ;
* tierce mineure (3m)
** quinte juste (5J)
*** septième mineure : sur les degrés ii, iii et vi, appelés « accords mineur septième » et notés Xm<sup>7</sup> ou X–<sup>7</sup> (X pour D, E ou A),
** quinte diminuée (5d)
*** septième mineure (7m) : sur le degré vii, appelé « accord demi-diminué » (puisque seule la quinte est diminuée) et noté X<sup>∅</sup> ou Xm<sup>7(♭5)</sup> ou X–<sup>7(♭5)</sup> (X pour B) ;<br /> en musique classique, on considère que c'est un accord de neuvième de dominante sans fondamentale.
Nous avons donc quatre types d'accords : X<sup>7</sup>, X<sup>maj7</sup>, Xm<sup>7</sup> et X<sup>∅</sup>
En jazz, on ajoute souvent la quarte à l'accord de sous-dominante IV (sur le ''fa'' dans une gamme de ''do'' majeur) ; il s'agit ici d'une quarte augmentée (''fa''-''si'') et l'accord est surnommé « accord lydien » mais cette dénomination est erronée (il s'agit d'une mauvaise interprétation de textes antiques). C'est un accord de onzième sans neuvième (la onzième étant l'octave de la quarte), il est noté X<sup>maj7(♯11)</sup> ou X<sup>Δ(♯11)</sup> (ici, F<sup>maj7(♯11)</sup>, ''fa''-''la''-''do''-''mi''-''si'' ou ''fa''-''la''-''si''-''do''-''mi'').
{| class="wikitable"
|+ Chiffrage jazz des accords de septième
|-
! scope="col" rowspan="2" | Tierce
! scope="col" rowspan="2" | Quinte
! scope="col" colspan="2" | Septième
|-
! scope="col" | 7m
! scope="col" | 7M
|-
| rowspan="2" | 3m
| 5d || X<sup>∅</sup>, X<sub>m</sub><sup>7(♭5)</sup>, X–<sup>7(♭5)</sup> ||
|-
| rowspan="2" | 5J
| X<sub>m</sub><sup>7</sup>, X–<sup>7</sup> ||
|-
| rowspan="2" | 3M
| X<sup>7</sup> || X<sup>maj7</sup>, X<sup>Δ</sup>
|-
| 5A || || X<sub>+</sub><sup>maj7</sup>, X<sub>+</sub><sup>Δ</sup>
|}
=== Modulation et emprunt ===
Un morceau peut comporter des changements de tonalité; appelés « modulation ». Il y a parfois un court passage dans une tonalité différente, typiquement sur une ou deux mesures, avant de retourner dans la tonalité d'origine : on parle d'emprunt. Lorsqu'il y a une modulation ou un emprunt, les degrés changent. Un même accord peut donc avoir une fonction dans une partie du morceau et une autre fonction ailleurs. L'utilisation d'accord différents, et en particulier d'accord utilisant des altérations accidentelles, indique clairement une modulation.
Nous avons vu précédemment que les modulations courantes sont :
* les modulations dans les tons voisins ;
* les modulations homonymes ;
* les marches harmoniques.
Une modulation entre une tonalité majeure et mineure change la couleur du passage,
* la modulation la plus « douce » est entre les tonalités relatives (par exemple''do'' majeur et ''la'' mineur) car ces tonalités utilisent quasiment les mêmes notes ;
* la modulation la plus « voyante » est la modulation homonyme (par exemple entre ''do'' majeur et ''do'' mineur).
Une modulation commence souvent sur l'accord de dominante de la nouvelle tonalité.
Pour analyser un œuvre, ou pour improviser sur une partie, il est important de reconnaître les modulations. La description de la successind es tonalités s'appelle le « parcours tonal ».
=== Exercices élémentaires ===
L'apprentissage des accords passe par quelques exercices élémentaires.
'''1. Lire un accord'''
Il s'agit de lecture de notes : des notes composant les accords sont écrites « empilées » sur une portée, il faut les lire en énonçant les notes de bas en haut.
'''2. Reconnaître la « couleur » d'un accord'''
On écoute une triade et il faut dire si c'est une triade majeure ou mineure. Puis, on complexifie l'exercice en ajoutant la septième.
'''3. Chiffrage un accord'''
Trouver le nom d'un accord à partir des notes qui le composent.
'''4. Réalisation d'un accord'''
Trouver les notes qui composent un accord à partir de son nom.
'''5. Dictée d'accords'''
On écoute une succession d'accords et il faut soit écrire les notes sur une portée, soit écrire les noms de accords.
[[File:Exercice constitution accord basse chiffree.svg|thumb|Exercice : constitution d'accord à partir de la basse chiffrée.]]
'''Exercices de basse chiffrée'''
''Réalisation d'un accord''
Sur la figure suivante, écrire les notes des accords correspondant à la basse chiffrée. Déterminer le degré de la fondamentale pour chaque accord en considérant que nous sommes dans la tonalité de ''sol'' majeur.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice constitution accord basse chiffree solution.svg|vignette|Solution.]]
# La note de basse est un ''do''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''mi'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''sol''.<br />Le chiffrage « <sup>5</sup> » indique que c'est un accord dans son état fondamental (l'écart entre deux notes consécutives ne dépasse pas la tierce), la fondamentale est donc la basse, ''do'', qui est le degré IV de la tonalité.
# La note de basse est un ''si''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''ré'', puis nous appliquons le chiffrage 6 et ajoutons la sixte, ''sol''.<br />Le chiffrage « <sup>6</sup> » indique que c'est un accord dans son premier renversement. En le remettant dans son état fondamental, nous obtenons ''sol-si-ré'', la fondamentale est donc la tonique, le degré I.
# La note de basse est un ''la''. Nous ajoutons la tierce (chiffre 3), ''do'', et la sixte (6), ''fa''♯. Nous vérifions que le ''fa''♯ est la sensible (signe +)<br />Nous voyons un « blanc » entre les notes ''do'' et ''fa''♯. En descendant le ''fa''♯ à l'octave inférieure, nous obtenons un empilement de tierces ''fa''♯-''la-do'', le fondamentale est donc ''fa''♯, le degré VII. Nous pouvons le voir comme le deuxième renversement de l'accord de septième de dominante, sans fondamentale.
# La note de basse est un ''fa''♯. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''la'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''do'' ; nous vérifions qu'il s'agit bien d'une quinte diminuée (le 5 est barré). Nous appliquons le chiffre 6 et ajoutons la sixte, ''ré''.<br />Nous voyons que les notes ''do'' et ''ré'' sont conjointes (intervalle de seconde). En descendant le ''ré'' à l'octave inférieure, nous obtenons un empilement de tierces ''ré-fa''♯-''la-do'', le fondamentale est donc ''ré'', le degré V. Nous constatons que l'accord chiffré est le premier renversement de l'accord de septième de dominante.
{{boîte déroulante/fin}}
{{clear}}
''Chiffrage d'accords''
[[Fichier:Exercice chiffrage accord basse chiffree.svg|vignette|Accords à chiffrer.]]
Chiffrer les accords ci-contre.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice chiffrage accord basse chiffree solution.svg|vignette|Solution.]]
# On relève les intervalles en partant de la basse : tierce majeure (3M) et quinte juste (5J). Le chiffrage complet est donc ''fa''<sup>5</sup><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''fa''<sup>5</sup>.<br /> On peut aussi reconnaître que c'est l'accord parfait sur la tonique de la tonalité de ''fa'' majeur dans son état fondamental, le chiffrage d'un accord parfait étant <sup>5</sup>.
# On relève les intervalles en partant de la basse : quarte juste (4J), sixte majeure (6M). Le chiffrage complet est donc ''fa''<sup>6</sup><sub>4</sub>.<br /> On peut aussi reconnaître que c'est le second renversement de l'accord ''mi-sol-si'', sur la tonique de la tonalité de ''mi'' mineur, le chiffrage du second renversement d'un accord parfait étant <sup>6</sup><sub>4</sub>.
# Les intervalles en partant de la basse sont : tierce mineure (3m), quinte diminuée (5d), sixte mineure (6m). Le chiffrage complet est donc ''mi''<sup>6</sup><small><s>5</s></small><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''mi''<sup>6</sup><sub><s>5</s></sub>.<br /> On reconnaît le premier renversement de l'accord ''do-mi-sol-si''♭, accord de septième de dominante de la tonalité de ''fa'' majeur.
# Les intervalles en partant de la basse sont : tierce mineure (3m), quinte juste (5J), septième mineure (7m). Le chiffrage complet est donc ''ré''<sup>7</sup><small>5</small><sub>3</sub> ; c'est typique d'un accord de septième de dominante, son chiffrage est donc ''ré''<sup>7</sup><sub>+</sub>.<br /> On reconnaît l'accord de septième de dominante de la tonalité de ''sol'' mineur dans son état fondamental.
{{boîte déroulante/fin}}
{{clear}}
[[File:Exercice constitution accord notation jazz.svg|thumb|Exercice : constitution d'un accord d'après son chiffrage en notation jazz.]]
'''Exercices de notation jazz'''
''Réalisation d'un accord''
Sur la figure suivante, écrire les notes des accords correspondant aux chiffrages.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice constitution accord notation jazz solution.svg|thumb|Solution.]]
# Il s'agit de la triade majeure de ''do'' dans son état fondamental. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''do-mi-sol''.
# Il s'agit de la triade majeure de ''sol''. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''sol-si-ré''. On renverse l'accord afin que la basse soit le ''si'', l'accord est donc ''si-ré-sol''.
# Il s'agit de l'accord demi-diminué de ''fa''♯. Les intervalles sont la tierce mineure (3m), la quinte diminuée (5d) et la septième mineure (7m). Les notes sont donc ''fa''♯-''la-do-mi''. Nous renversons l'accord afin que la basse soit le ''la'', l'accord est donc ''a-do-mi-fa''♯.
# Il s'agit de l'accord de septième de ''ré''. Les intervalles sont donc la tierce majeure (3M), la quinte juste (5J) et la septième mineure (7m). Les notes sont ''ré-fa''♯''-la-do''. Nous renversons l'accord afin que la basse soit le ''fa''♯, l'accord est donc ''fa''♯''-la-do-ré''.
{{boîte déroulante/fin}}
{{clear}}
''Chiffrage d'accords''
[[File:Exercice chiffrage accord notation jazz.svg|thumb|Accords à chiffrer.]]
Chiffrer les accords ci-contre.
{{boîte déroulante/début|titre=Solution}}
[[File:Exercice chiffrage accord notation jazz solution.svg|thumb|Solution.]]
# Les notes sont toutes sur des interlignes consécutifs, c'est donc un empilement de tierces ; l'accord est dans son état fondamental. Les intervalles sont une tierce majeure (''fa-la'' : 3M) et une quinte juste (''fa-do'' : 5J), c'est donc la triade majeure de ''fa''. Le chiffrage est F.
# Il y a un blanc dans l'empilement des notes, c'est donc un accord renversé. En permutant les notes pour n'avoir que des tierces, on trouve l'accord ''mi-sol-si''. Les intervalles sont une tierce mineure (''mi-sol'' : 3m) et une quinte juste (''mi-si'' : 5J), c'est donc la triade mineure de ''mi'' avec un ''si'' à la basse. Le chiffrage est Em/B ou E–/B.
# Il y deux notes conjointes, c'est donc un renversement. L'état fondamental de cet accord est ''do-mi-sol-si''♭. Les intervalles sont une tierce majeure (''do-mi'' : 3M), une quinte juste (''do-sol'' : 5J) et une septième mineure (''do-si''♭ : 7m). C'est donc l'accord de ''do'' septième avec un ''mi'' à la basse, chiffré C<sup>7</sup>/E.
# Les notes sont toutes sur des interlignes consécutifs, l'accord est dans son état fondamental. Les intervalles sont la tierce mineure (''ré-fa'' : 3m), une quinte juste (''ré-la'' : 5J) et une septième mineure (''ré-do'' : 7m). C'est donc l'accord de ''ré'' mineur septième, chiffré Dm<sup>7</sup> ou D–<sup>7</sup>.
{{boîte déroulante/fin}}
{{clear}}
== Harmonie fonctionnelle ==
Le choix des accords et de leur succession — la progression des accords — est un élément important d'un morceau, de sa composition. Le compositeur ou la compositrice a bien sûr une liberté totale, mais pour faire des choix, il faut comprendre les conséquences de ces choix, et donc ici, les effets produits par les accords et leur progression.
Une des manières d'aborder le sujet est l'harmonie fonctionnelle.
=== Les trois fonctions des accords ===
En harmonie tonale, on considère que les accords ont une fonction. Il existe trois fonctions :
* la fonction de tonique, {{Times New Roman|I}} ;
* la fonction de sous-dominante, {{Times New Roman|IV}} ;
* la fonction de dominante, {{Times New Roman|V}}.
L'accord de tonique, {{Times New Roman|I}}, est l'accord « stable » de la tonalité par excellence. Il conclut en général les morceaux, et ouvre souvent les morceaux ; il revient fréquemment au cours du morceau.
L'accord de dominante, {{Times New Roman|V}}, est un accord qui introduit une instabilité, une tension. En particulier, il contient la sensible (degré {{Times New Roman|VI}}), qui est une note « aspirée » vers la tonique. Cette tension, qui peut être renforcée par l'utilisation d'un accord de septième, est fréquemment résolue par un passage vers l'accord de tonique. Nous avons donc deux mouvements typiques : {{Times New Roman|I}} → {{Times New Roman|V}} (création d'une tension, d'une attente) et {{Times New Roman|V}} → {{Times New Roman|I}} (résolution d'une tension). Les accords de tonique et de dominante ont le cinquième degré en commun, cette note sert donc de pivot entre les deux accords.
L'accord de sous-dominante, {{Times New Roman|IV}}, est un accord qui introduit lui aussi une tension, mais moins grande : il ne contient pas la sensible. Notons que s'il est une quarte au-dessus de la tonique, il est aussi une quinte en dessous d'elle ; il est symétrique de l'accord de dominante. Il a donc un rôle similaire à l'accord de dominante, mais atténué. L'accord de sous-dominante aspire soit vers l'accord de dominante, très proche, et l'on a alors une augmentation de la tension ; soit vers l'accord de tonique, un retour vers la stabilité (il a alors un rôle semblable à la dominante). Du fait de ces deux bifurcations possibles — augmentation de la tension ({{Times New Roman|IV}} → {{Times New Roman|V}}) ou retour à la stabilité ({{Times New Roman|IV}} → {{Times New Roman|I}}) —, l'utilisation de l'accord de sous-dominante introduit un certain flottement : si l'on peut facilement prédire l'accord qui suit un accord de dominante, on ne peut pas prédire ce qui suit un accord de sous-dominante.
Notons que la composition ne consiste pas à suivre ces règles de manière stricte, ce qui conduirait à des morceaux stéréotypés et plats. Le plaisir d'écoute joue sur une alternance entre satisfaction d'une attente (respect des règles) et surprise (rompre les règles).
=== Accords remplissant ces fonctions ===
Les accords sur les autres degrés peuvent se ramener à une de ces trois fonctions :
* {{Times New Roman|II}} : fonction de sous-dominante {{Times New Roman|IV}} ;
* {{Times New Roman|III}} (très peu utilisé en mode mineur en raison de sa dissonance) et {{Times New Roman|VI}} : fonction de tonique {{Times New Roman|I}} ;
* {{Times New Roman|VII}} : fonction de dominante {{Times New Roman|V}}.
En effet, les accords étant des empilements de tierces, des accords situés à une tierce l'un de l'autre — {{Times New Roman|I}} ↔ {{Times New Roman|III}}, {{Times New Roman|II}} ↔ {{Times New Roman|IV}}, {{Times New Roman|V}} ↔ {{Times New Roman|VII}}, {{Times New Roman|VI}} ↔ {{Times New Roman|VIII}} ( = {{Times New Roman|I}}) — ont deux notes en commun. On retrouve le fait que l'accord sur le degré {{Times New Roman|VII}} est considéré comme un accord de dominante sans tonique. En mode mineur, l'accord sur le degré {{Times New Roman|III}} est évité, il n'a donc pas de fonction.
{|class="wikitable"
|+ Fonction des accords
|-
! scope="col" | Fondamentale
! scope="col" | Fonction
|-
| {{Times New Roman|I}} || tonique
|-
| {{Times New Roman|II}} || sous-dominante faible
|-
| {{Times New Roman|III}} || tonique faible
|-
| {{Times New Roman|IV}} || sous-dominante
|-
| {{Times New Roman|V}} || dominante
|-
| {{Times New Roman|VI}} || tonique faible
|-
| {{Times New Roman|VII}} || dominante faible
|}
Par exemple en ''do'' majeur :
* fonction de tonique : '''''do''<sup>5</sup> (C)''', ''mi''<sup>5</sup> (E–), ''la''<sup>5</sup> (A–) ;
* fonction de sous-dominante : '''''fa''<sup>5</sup> (F)''', ''ré''<sup>5</sup> (D–) ;
* fonction de dominante : '''''sol''<sup>5</sup> (G)''' ou ''sol''<sup>7</sup><sub>+</sub> (G<sup>7</sup>), ''si''<sup> <s>5</s></sup> (B<sup>o</sup>).
En ''la'' mineur harmonique :
* fonction de tonique : '''''la''<sup>5</sup> (A–)''', ''fa''<sup>5</sup> (F) [, rarement : ''do''<sup>+5</sup> (C<sup>+</sup>)] ;
* fonction de sous-dominante : '''''ré''<sup>5</sup> (D–)''', ''si''<sup> <s>5</s></sup> (B<sup>o</sup>) ;
* fonction de dominante : '''''mi''<sup>5</sup> (E)''' ou ''mi''<sup>7</sup><sub>+</sub> (E<sup>7</sup>), ''sol''♯<sup> <s>5</s></sup> (G♯<sup>o</sup>).
Le fait d'utiliser des accords différents pour remplir une fonction permet d'enrichir l'harmonie, et de jouer sur l'équilibre entre satisfaction d'une attente (on respecte les règles sur les fonctions) et surprise (mais on n'utilise pas l'accord attendu).
=== Les dominantes secondaires ===
On utilise aussi des accords de septième dominante se fondant sur un autre degré que la dominante de la gamme ; on parle de « dominante secondaire ». Typiquement, avant un accord de septième de dominante, on utilise parfois un accord de dominante de dominante, dont le degré est alors noté « {{Times New Roman|V}} de {{Times New Roman|V}} » ou « {{Times New Roman|V}}/{{Times New Roman|V}} » ; la fondamentale est de l'accord est alors situé cinq degrés au-dessus de la dominante ({{Times New Roman|V}}), c'est donc le degré {{Times New Roman|IX}}, c'est-à-dire le degré {{Times New Roman|II}} de la tonalité en cours). Ou encore, on utilise un accord de dominante du degré {{Times New Roman|IV}} (« {{Times New Roman|V}} de {{Times New Roman|IV}} », la fondamentale est alors le degré {{Times New Roman|I}}) avant un accord sur le degré {{Times New Roman|IV}} lui-même.
Par exemple, en tonalité de ''do'' majeur, on peut trouver un accord ''ré - fa''♯'' - la - do'' (chiffré {{Times New Roman|V}} de {{Times New Roman|V}}<sup>7</sup><sub>+</sub>), avant un accord ''sol - si - ré - fa'' ({{Times New Roman|V}}<sup>7</sup><sub>+</sub>). L'accord ''ré - fa''♯'' - la - do'' est l'accord de septième de dominante des tonalités de ''sol''. Dans la même tonalité, on pourra utiliser un accord ''do - mi - sol - si''♭ ({{Times New Roman|V}} de {{Times New Roman|IV}}<sup>7</sup><sub>+</sub>) avant un accord ''fa - la - do'' ({{Times New Roman|IV}}<sup>5</sup>). Le recours à une dominante secondaire peut atténuer une transition, par exemple avec un enchaînement ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> → ''fa''<sup>5</sup> (C → C<sup>7</sup> → F) qui correspond à un enchaînement {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}} : le passage ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> (C → C<sup>7</sup>) se fait en ajoutant une note (le ''si''♭) et rend naturel le passage ''do'' → ''fa''.
Sur les sept degré de la gamme, on ne considère en général que cinq dominantes secondaires : en effet, la dominante du degré {{Times New Roman|I}} est la dominante « naturelle, primaire » de la tonalité (et n'est donc pas secondaire) ; et utiliser la dominante de {{Times New Roman|VII}} consisterait à considérer l'accord de {{Times New Roman|VII}} comme un accord propre, on évite donc les « {{Times New Roman|V}} de “{{Times New Roman|V}}” » (mais les « “{{Times New Roman|V}}” de {{Times New Roman|V}} » sont tout à fait « acceptables »).
=== Enchaînements classiques ===
Nous avons donc vu que l'on trouve fréquemment les enchaînements suivants :
* pour créer une instabilité :
** {{Times New Roman|I}} → {{Times New Roman|V}},
** {{Times New Roman|I}} → {{Times New Roman|IV}} (instabilité moins forte mais incertitude sur le sens d'évolution) ;
* pour maintenir l'instabilité :
** {{Times New Roman|IV}} → {{Times New Roman|V}} ;
* pour résoudre l'instabilité :
** {{Times New Roman|IV}} → {{Times New Roman|I}},
** {{Times New Roman|V}} → {{Times New Roman|I}}, cas particuliers (voir plus bas) :
*** {{Times New Roman|V}}<sup>+4</sup> → {{Times New Roman|I}}<sup>6</sup>,
*** {{Times New Roman|I}}<sup>6</sup><sub>4</sub> → {{Times New Roman|V}}<sup>7</sup><sub>+</sub> → {{Times New Roman|I}}<sup>5</sup>.
Les degrés indiqués ci-dessus sont les fonctions ; on peut donc utiliser les substitutions suivantes :
* {{Times New Roman|I}} par {{Times New Roman|VI}} et, en tonalité majeure, {{Times New Roman|III}} ;
* {{Times New Roman|IV}} par {{Times New Roman|II}} ;
* {{Times New Roman|V}} par {{Times New Roman|VII}}.
Pour enrichir l'harmonie, on peut utiliser les dominantes secondaires, en particulier :
* {{Times New Roman|V}} de {{Times New Roman|V}} ({{Times New Roman|II}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|V}},
* {{Times New Roman|V}} de {{Times New Roman|IV}} ({{Times New Roman|I}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|IV}}.
On peut enchaîner les enchaînements, par exemple {{Times New Roman|I}} → {{Times New Roman|IV}} → {{Times New Roman|V}}, ou encore {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}}… En jazz, on utilise très fréquemment l'enchaînement {{Times New Roman|II}} → {{Times New Roman|V}} → {{Times New Roman|I}} (deux-cinq-un).
On peut bien sûr avoir d'autres enchaînements, mais ces règles permettent d'analyser un grand nombre de morceaux, et donnent des clefs utiles pour la composition. Nous voyons ci-après un certain nombre d'enchaînements courants dans différents styles
== Exercice ==
Un hautboïste travaille la sonate en ''do'' mineur S. 277 de Heinichen. Sur le deuxième mouvement ''Allegro'', il a du mal à travailler un passage en raison des altérations accidentelles. Sur la suggestion de sa professeure, il décide d'analyser la progression d'accords sous-jacente afin que les altérations deviennent logiques. Il s'agit d'un duo hautbois et basson pour lequel les accords ne sont pas chiffrés, le basson étant ici un instrument soliste et non pas un élément de la basse continue.
Sur l'extrait suivant, déterminez les basses et la qualité (chiffrage) des accords sous-jacents. Commentez.
[[Fichier:Sonate hautbois basson heinichen 2e mvt mes49.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen.]]
{{note|L'œuvre est en ''do'' mineur et devrait donc avoir trois bémols à la clef, or ici il n'y en a que deux. En effet, le ''la'' pouvant être bécarre en mode mineur mélodique ascendant, le compositeur a préféré le noter explicitement en altération accidentelle lorsque l'on est en mode mélodique naturel, harmonique ou mélodique descendant. C'est un procédé assez courant à l'époque baroque.}}
{{boîte déroulante/début|titre=Solution}}
Une des difficultés ici est que les arpèges joués par les instruments sont agrémentés de notes de passage.
Les notes de la basse (du basson) sont différentes entre le premier et le deuxième temps de chaque mesure et ne peuvent pas appartenir au même accord. On a donc un accord par temps.
Sur le premier temps de chaque mesure, le basson joue une octave. La note concernée est donc la basse de chaque accord. Pour savoir s'il s'agit d'un accord à l'état fondamental ou d'un renversement, on regarde ce que joue le hautbois : dans un mouvement conjoint (succession d'intervalles de secondes), il est difficile de distinguer les notes de l'arpège des notes de passage, mais
: les notes des grands intervalles font partie de l'accord.
Ainsi, sur le premier temps de la première mesure (la basse est un ''mi''♭), on a une sixte descendante ''sol''-''si''♭ et, à la fin du temps, une tierce descendante ''sol''-''mi''♭. L'accord est donc ''mi''♭-''sol''-''si''♭, c'est un accord de quinte (accord parfait à l'état fondamental). À la fin du premier temps, le basson joue un ''do'', c'est donc une note étrangère.
Sur le second temps de la première mesure, le basson joue une tierce ascendante ''fa''-''la''♭, la première note est la basse de l'accord et la seconde une des notes de l'accord. Le hautbois commence par une sixte descendante ''la''♭-''do'', l'accord est donc ''fa''-''la''♭-''do'', un accord de quinte (accord parfait à l'état fondamental). Le ''do'' du basson la fin du premier temps est donc une anticipation.
Les autres notes étrangères de la première mesure sont des notes de passage.
Mais il faut faire attention : en suivant ce principe, sur les premiers temps des deuxième et troisième mesure, nous aurions des accords de septième d'espèce (puisque la septième est majeure). Or, on ne trouve pas, ou alors exceptionnellement, d'accord de septième d'espèce dans le baroque, mais quasi exclusivement des accords de septième de dominante. Donc au début de la deuxième mesure, le ''la''♮ est une appoggiature du ''si''♭, l'accord est donc ''si''♭-''ré''-''fa'', un asscord de quinte. De même, au début de la troisième mesure, le ''sol'' est une appoggiature du ''la''♭.
Il faut donc se méfier d'une analyse purement « mathématique ». Il faut s'attacher à ressentir la musique, et à connaître les styles, pour faire une analyse pertinente.
Ci-dessous, nous avons grisé les notes étrangères.
[[Fichier:Sonate hautbois basson heinichen 2e mvt mes49 analyse.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen. Analyse de la progression harmonique.]]
Le chiffrage jazz équivalent est :
: | E♭ F– | B♭<sup>Δ</sup> E♭ | A♭<sup>Δ</sup> D– | G …
Nous remarquons une progression assez régulière :
: ''mi''♭ ↗[2<sup>de</sup>] ''fa'' | ↘[5<sup>te</sup>] ''si''♭ ↗[4<sup>te</sup>] ''mi''♭ | ↘[5<sup>te</sup>] ''la''♭ ↗[4<sup>te</sup>] ''ré'' | ↘[5<sup>te</sup>] ''sol''
Le ''mi''♭ est le degré {{Times New Roman|III}} de la tonalité principale (''do'' mineur), c'est donc une tonique faible ; il « joue le même rôle » qu'un ''do''. S'il y avait eu un accord de ''do'' au début de l'extrait, on aurait eu une progression parfaitement régulière ↗[4<sup>te</sup>] ↘[5<sup>te</sup>].
Nous avons les modulations suivantes :
* mesure 49 : ''do'' mineur naturel (le ''si''♭ n'est pas une sensible) avec un accord sur “{{Times New Roman|I}}” (tonique faible, {{Times New Roman|III}}, pour la première analyse, ou bien tonique forte, {{Times New Roman|I}}, pour la seconde) suivi d'un accord sur {{Times New Roman|IV}} ;
* mesure 50 : ''si''♭ majeur avec un accord sur {{Times New Roman|I}} suivi d'un accord sur {{Times New Roman|IV}} ;
* mesure 51 : ''la''♭ majeur avec un accord sur {{Times New Roman|I}}, et emprunt à ''do'' majeur avec un accord sur {{Times New Roman|II}} ({{Times New Roman|IV}} faible).
On a donc une marche harmonique {{Times New Roman|I}} → {{Times New Roman|IV}} qui descend d'une seconde majeure (un ton) à chaque mesure (''do'' → ''si''♭ → ''la''♭), avec une exception sur la dernière mesure (modulation en cours de mesure et descente d'une seconde mineure au lieu de majeure).
Ce passage est donc construit sur une régularité, une règle qui crée un effet d'attente — enchaînement {{Times New Roman|I}}<sup>5</sup> → {{Times New Roman|IV}}<sup>5</sup> avec une marche harmonique d'une seconde majeure descendante —, et des « surprises », des exceptions au début — ce n'est pas un accord {{Times New Roman|I}}<sup>5</sup> mais un accord {{Times New Roman|III}}<sup>5</sup> — et à la fin — modulation en milieu de mesure et dernière descente d'une seconde mineure (½t ''la''♭ → ''sol'').
L'extrait ne permet pas de le deviner, mais la mesure 52 est un retour en ''do'' mineur, avec donc une modulation sur la dominante (accord de ''sol''<sup>7</sup><sub>+</sub>, G<sup>7</sup>).
{{boîte déroulante/fin}}
== Progression d'accords ==
Comme pour la mélodie, la succession des accords dans un morceau, la progression d'accords, suit des règles. Et comme pour la mélodie, les règles diffèrent d'un style musical à l'autre et la créativité consiste à parfois ne pas suivre ces règles. Et comme pour la mélodie, on part d'un ensemble de notes organisé, d'une gamme caractéristique d'une tonalité, d'un mode.
Les accords les plus utilisés pour une tonalité donnée sont les accords dont la fondamentale sont les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la tonalité, en particulier la triade {{Times New Roman|I}}, appelée « accord parfait » ou « accord de tonique », et l'accord de septième {{Times New Roman|V}}, appelé « septième de dominante ».
Le fait d'avoir une progression d'accords qui se répète permet de structurer un morceau. Pour les morceaux courts, il participe au plaisir de l'écoute et facilite la mémorisation (par exemple le découpage couplet-refrain d'une chanson). Sur les morceaux longs, une trop grande régularité peut introduire de la lassitude, les longs morceaux sont souvent découpés en parties présentant chacune une progression régulière. Le fait d'avoir une progression régulière permet la pratique de l'improvisation : cadence en musique classique, solo en jazz et blues.
; Note
: Le terme « cadence » désigne plusieurs choses différentes, et notamment en harmonie :
:* une partie improvisée dans un opéra ou un concerto, sens utilisé ci-dessus ;
:* une progression d'accords pour ponctuer un morceau et en particulier pour le conclure, sens utilisé dans la section suivante.
=== Accords peu utilisés ===
En mode mineur, l'accord de quinte augmentée {{Times New Roman|III<sup>+5</sup>}} est très peu utilisé. C'est un accord dissonant ; il intervient en général comme appogiature de l'accord de tonique (par exemple en ''la'' mineur : {{Times New Roman|III<sup>+5</sup>}} ''do'' - ''mi'' - ''sol''♯ → {{Times New Roman|I<sup>6</sup>}} ''do'' - ''mi'' - ''la''), ou de l'accord de dominante ({{Times New Roman|III<sup>6</sup><sub>+3</sub>}} ''mi'' - ''sol''♯ - ''do'' → {{Times New Roman|V<sup>5</sup>}} ''mi'' - ''sol''♯ - ''si''). Il peut être aussi utilisé comme préparation à l'accord de sous-dominante (enchaînement {{Times New Roman|III}} → {{Times New Roman|IV}}). Par ailleurs, il a une constitution symétrique — c'est l'empilement de deux tierces majeures — et ses renversements ont les mêmes intervalles à l'enharmonie près (quinte augmentée/sixte mineure, tierce majeure/quarte diminuée). De ce fait, un même accord est commun, par renversement et à l'enharmonie près, à trois tonalités : le premier renversement de l'accord ''do'' - ''mi'' - ''sol''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur) est enharmonique à ''mi'' - ''sol''♯ - ''si''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''do''♯ mineur) ; le second renversement est enharmonique à ''la''♭ - ''do'' - ''mi'' ({{Times New Roman|III}}<sup>e</sup> degré de ''fa'' mineur).
=== Accords très utilisés ===
Les trois accords les plus utilisés sont les accords de tonique (degré {{Times New Roman|I}}), de sous-dominante ({{Times New Roman|IV}}) et de dominante ({{Times New Roman|V}}). Ils interviennent en particulier en fin de phrase, dans les cadences. L'accord de dominante sert souvent à introduire une modulation : la modulation commence sur l'accord de dominante de la nouvelle tonalité. On note que l'accord de sous-dominante est situé une quinte juste en dessous de la tonique, les accords de dominante et de sous-dominante sont donc symétriques.
En jazz, on utilise également très fréquemment l'accord de la sus-tonique (degré {{Times New Roman|II}}), souvent dans des progressions {{Times New Roman|II}} - {{Times New Roman|V}} (- {{Times New Roman|I}}). Rappelons que l'accord de sus-tonique a la fonction de sous-dominante.
=== Cadences et ''turnaround'' ===
Le terme « cadence » provient de l'italien ''cadenza'' et désigne la « chute », la fin d'un morceau ou d'une phrase musicale.
On distingue deux types de cadences :
* les cadences conclusive, qui créent une sensation de complétude ;
* les cadences suspensives, qui crèent une sensation d'attente.
==== Cadence parfaite ====
[[Fichier:Au clair de le lune cadence parfaite.midi|thumb|''Au clair de la lune'', harmonisé avec une cadence parfaite (italienne).]]
[[Fichier:Au clair de le lune mineur cadence parfaite.midi|thumb|''Idem'' mais en mode mineur harmonique.]]
La cadence parfaite est l'enchaînement de l'accord de dominante suivi de l'accord parfait : {{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}}, les deux accord étant à l'état fondamental. Elle donne une impression de stabilité et est donc très souvent utilisée pour conclure un morceau. C'est une cadence conclusive.
On peut aussi utiliser l'accord de septième de dominante, la dissonance introduisant une tension résolue par l'accord parfait : {{Times New Roman|V<sup>7</sup><sub>+</sub> - I<sup>5</sup>}}.
Elle est souvent précédée de l'accord construit sur le IV<sup>e</sup> degré, appelé « accord de préparation », pour former la cadence italienne : {{Times New Roman|IV<sup>5</sup> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}}.
Elle est également souvent précédée du second renversement de l'accord de tonique, qui est alors appelé « appoggiature de la cadence » : {{Times New Roman|I<sup>6</sup><sub>4</sub> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}} (on remarque que les accords {{Times New Roman|I}}<sup>6</sup><sub>4</sub> et {{Times New Roman|V}}<sup>5</sup> ont la basse en commun, et que l'on peut passer de l'un à l'autre par un mouvement conjoint sur les autres notes).
{{clear}}
==== Demi-cadence ====
[[Fichier:Au clair de le lune demi cadence.midi|thumb|''Au clair de la lune'', harmonisé avec une demi-cadence.]]
Une demi-cadence est une phrase ou un morceau se concluant sur l'accord construit sur le cinquième degré. Il provoque une sensation d'attente, de suspens. Il s'agit en général d'une succession {{Times New Roman|II - V}} ou {{Times New Roman|IV - V}}. C'est une cadence suspensive. On uilise rarement un accord de septième de dominante.
{{clear}}
==== Cadence rompue ou évitée ====
La cadence rompue, ou cadence évitée, est succession d'un accord de dominante et d'un accord de sus-dominante, {{Times New Roman|V}} - {{Times New Roman|VI}}. C'est une cadence suspensive.
==== Cadence imparfaite ====
Une cadence imparfaite est une cadence {{Times New Roman|V - I}}, comme la cadence parfaite, mais dont au moins un des deux accords est dans un état renversé.
==== Cadence plagale ====
La cadence plagale — du grec ''plagios'', oblique, en biais — est la succession de l'accord construit sur le quatrième degré, suivi de l'accord parfait : {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}. Elle peut être utilisée après une cadence parfaite ({{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}} - {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}). Elle donne un caractère solennel, voire religieux — elle est parfois appelée « cadence amen » —, elle a un côté antique qui rappelle la musique modale et médiévale<ref>{{lien web |url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/la-cadence-amen-ou-comment-se-dire-adieu-7191921 |titre=La cadence « Amen » ou comment se dire adieu |auteur=Max Dozolme (MAXXI Classique) |site=France Musique |date=2025-04-25 |consulté le=2025-04-25}}.</ref>.
C'est une cadence conclusive.
==== {{lang|en|Turnaround}} ====
[[Fichier:Au clair de le lune turnaround.midi|thumb|Au clair de la lune, harmonisé en style jazz : accords de 7{{e}}, anatole suivie d'un ''{{lang|en|turnaround}}'' ii-V-I.]]
Le terme ''{{lang|en|turnaround}}'' signifie revirement, retournement. C'est une succession d'accords que fait la transition entre deux parties, en créant une tension-résolution. Le ''{{lang|en|turnaround}}'' le plus courant est la succession {{Times New Roman|II - V - I}}.
On utilise également fréquemment l'anatole : {{Times New Roman|I - VI - II - V}}.
{| class="wikitable"
|+ Progressions typiques d'accords dans une tonalité majeure
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Cadence<br />parfaite<br /> {{Times New Roman|V - I}}
! scope="col" | Cadence<br />italienne<br />{{Times New Roman|IV - V - I}}
! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou IV - V}}
! scope="col" | Cadence<br />plagale<br />{{Times New Roman|IV - I}}
! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}}
! scope="col" | Anatole<br />{{Times New Roman|I - vi - ii - V}}
|-
|''Do'' majeur || || G - C || F - G - C || Dm - G ou F - G || F - C || Dm - G - C || C - Am - Dm - G
|-
|''Sol'' majeur || ''fa''♯ || D - G || C - D - G || Am - D ou C - D || C - G || Am - D - G || G - Em - Am - D
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || A - D || G - A - D || Em - A ou G - A || G - D || Em - A - D || D - Bm - Em - A
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || E - A || D - E - A || Bm - E ou D - E || D - A || Bm - E - A || A - F♯m - B - E
|-
| ''Fa'' majeur || ''si''♭ || C - F || B♭ - C - F || Gm - C ou B♭ - C || B♭ - F || Gm - C - F || F - Dm - Gm - C
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || F - B♭ || E♭ - F - B♭ || Cm - F ou E♭ - F || E♭ - B♭ || Cm - F - B♭ || B♭ - Gm - Cm - F
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || B♭ - E♭ || A♭ - B♭ - E♭ || Fm - B♭ ou A♭ - B♭ || A♭ - E♭ || Fm - B♭ - E♭ || Gm - Cm - Fm - B♭
|}
{| class="wikitable"
|+ Progressions typiques d'accords dans une tonalité mineure
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" | Cadence<br />parfaite<br />{{Times New Roman|V - i}}
! scope="col" | Cadence<br />italienne<br />{{Times New Roman|iv - V - i}}
! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou iv - V}}
! scope="col" | Cadence<br />plagale<br />{{Times New Roman|iv - i}}
! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}}
! scope="col" | Anatole<br />{{Times New Roman|i - VI - ii - V}}
|-
| ''La'' mineur<br />harmonique || || E - Am || Dm - E - Am || B° - E ou Dm - E || Dm - Am || B° - E - Am || Am - F - B° - E
|-
| ''Mi'' mineur<br />harmonique || ''fa''♯ || B - Em || Am - B - Em || F♯° - B ou Am - B || Am - Em || F♯° - B - Em || Em - C - F♯° - B
|-
| ''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || F♯ - Bm || Em - F♯ - Bm || C♯° - F♯ ou Em - F♯ || Em - Bm || C♯° - F♯ - Bm || Bm - G - C♯° - F♯
|-
| ''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || C♯ - F♯m || Bm - C♯ - F♯m || G♯° - C♯ ou Bm - C♯ || Bm - F♯m || G♯° - C♯ - F♯m || A+ - D - G♯° - C♯
|-
| ''Ré'' mineur<br />harmonique || ''si''♭ || A - Dm || Gm - A - Dm || E° - A ou Gm - A || Gm - Dm || E° - A - Dm || Dm - B♭ - E° - A
|-
| ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || D - Gm || Cm - D - Gm || A° - D ou Cm - D || Cm - Gm|| A° - D - Gm || Gm - E♭ - A° - D
|-
| ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || G - Cm || Fm - G - Cm || D° - G ou Fm - G || Fm - Dm || D° - G - Cm || Cm - A♭ - D° - G
|}
==== Exemple : ''La Mer'' ====
: {{lien web
| url = https://www.youtube.com/watch?v=PXQh9jTwwoA
| titre = Charles Trenet - La mer (Officiel) [Live Version]
| site = YouTube
| auteur = Charles Trenet
| consulté le = 2020-12-24
}}
Le début de ''La Mer'' (Charles Trenet, 1946) est en ''do'' majeur et est harmonisé par l'anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (C - Am - Dm - G<sup>7</sup>) sur deux mesures, jouée deux fois ({{Times New Roman|1=<nowiki>|I-vi|ii-V</nowiki><sup>7</sup><nowiki>|</nowiki>}} × 2). Viennent des variations avec les progressions {{Times New Roman|I-III-vi-V<sup>7</sup>}} (C - E - Am - G<sup>7</sup>) puis la « progression ’50s » (voir plus bas) {{Times New Roman|I-vi-IV-VI<sup>7</sup>}} (C - Am - F - A<sup>7</sup>, on remarque que {{Times New Roman|IV}}/F est le relatif majeur du {{Times New Roman|ii}}/Dm de l'anatole), jouées chacune une fois sur deux mesure ; puis cette première partie se conclut par une demie cadence {{Times New Roman|ii-V<sup>7</sup>}} sur une mesure puis une dernière anatole sur trois mesures ({{Times New Roman|1=<nowiki>|I-vi|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}}). Cela constitue une première partie « A » sur douze mesures qui se termine par une demi-cadence ({{Times New Roman|ii-V<sup>7</sup>}}) qui appelle une suite. Cette partie A est jouée une deuxième fois mais la fin est modifiée pour la transition : les deux dernières mesures {{Times New Roman|<nowiki>|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}} deviennent {{Times New Roman|<nowiki>|ii-V</nowiki><sup>7</sup><nowiki>|I|</nowiki>}} (|Dm-G7|C|), cette partie « A’ » se conclut donc par une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}).
Le morceau passe ensuite en tonalité de ''mi'' majeur, donc une tierce au dessus de ''do'' majeur, sur six mesures. Cette partie utilise une progression ’50s {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (E - C♯m - A - B<sup>7</sup>), qui est rappelons-le une variation de l'anatole, l'accord {{Times New Roman|ii}} (Fm) étant remplacé par son relatif majeur {{Times New Roman|IV}} (A). Cette anatole modifiée est jouée deux fois puis la partie en ''mi'' majeur se conclut par l'accord parfait {{Times New Roman|I}} joué sur deux mesures (|E|E|), on a donc, avec la mesure précédente, avec une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}).
Suivent ensuite six mesures en ''sol'' majeur, donc à nouveau une tierce au dessus de ''mi'' majeur. Elle comporte une progression {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (G - Em - C - D<sup>7</sup>), donc anatole avec substitution du {{Times New Roman|ii}}/Am par son relatif majeur {{Times New Roman|VI}}/C (progression ’50s), puis une anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (G - Em - Am - D<sup>7</sup>) et deux mesure sur la tonique {{Times New Roman|I-I<sup>7</sup>}} (G - G<sup>7</sup>), formant à nouveau une cadence parfaite. La fin sur un accord de septième, dissonant, appelle une suite.
Cette partie « B » de douze mesures comporte donc deux parties similaires « B1 » et « B2 » qui forment une marche harmonique (montée d'une tierce).
Le morceau se conclut par une reprise de la partie « A’ » et se termine donc par une cadence parfaite.
Nous avons une structure A-A’-B-A’ sur 48 mesures, proche la forme AABA étudiée plus loin.
Donc ''La Mer'' est un morceau structuré autour de l'anatole avec des variations (progression ’50s, substitution du {{Times New Roman|ii}} par son relatif majeur {{Times New Roman|IV}}) et comportant une marche harmonique dans sa troisième partie. Les parties se concluent par des ''{{lang|en|turnarounds}}'' sous la forme d'une cadence parfaite ou, pour la partie A, par une demi-cadence.
{| border="1" rules="rows" frame="hsides"
|+ Structure de ''La Mer''
|- align="center"
|
| colspan="12" | ''do'' majeur
|
|- align="center"
! scope="row" rowspan=2 | A
| colspan="2" | anatole
| colspan="2" | //
| colspan="2" | variation
| colspan="2" | ’50s
| ½ c.
| colspan="3" | anatole
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii}} || <nowiki>|</nowiki> {{Times New Roman|V<sup>7</sup>}} || <nowiki>|</nowiki>
|- align="center"
! scope="row" rowspan="2" | A’
| colspan="2" | anatole
| colspan="2" | //
| colspan="2" | variation
| colspan="2" | ’50s
| ½ c.
| colspan="2" | anatole
| c.p.
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki>
|- align="center"
|
| colspan="6" | B1 : ''mi'' majeur
| colspan="6" background="lightgray" | B2 : ''sol'' majeur
|
|- align="center"
! scope="row" rowspan="2" | B
| colspan="2" | ’50s
| colspan="2" | //
|colspan="2" | c.p.
| colspan="2" | ’50s
| colspan="2" | //
|colspan="2" | c.p.
|
|-
| <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I<sup>7</sup>}} || <nowiki>|</nowiki>
|-
! scope="row" | A’
| colspan="12" |
|
|}
=== Progression blues ===
La musique blues est apparue dans les années 1860. Elle est en général bâtie sur une grille d'accords ''({{lang|en|changes}})'' immuable de douze mesures ''({{lang|en|twelve-bar blues}})''. C'est sur cet accompagnement qui se répète que s'ajoute la mélodie — chant et solo. Cette structure est typique du blues et se retrouve dans ses dérivés comme le rock 'n' roll.
Le rythme est toujours un rythme ternaire syncopé ''({{lang|en|shuffle, swing, groove}}, ''notes inégales'')'' : la mesure est à quatre temps, mais la noire est divisée en noire-croche en triolet, ou encore triolet de croche en appuyant la première et la troisième.
La mélodie se construit en général sur une gamme blues de six degrés (gamme pentatonique mineure avec une quarte augmentée), mais bien que la gamme soit mineure, l'harmonie est construite sur la gamme majeure homonyme : un blues en ''fa'' a une mélodie sur la gamme de ''fa'' mineur, mais une harmonie sur la gamme de ''fa'' majeur. La grille d'accord comporte les accords construits sur les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la gamme majeure homonyme. Les accords sont souvent des accords de septième (donc avec une tierce majeure et une septième mineure), il ne s'agit donc pas d'une harmonisation de gamme diatonique (puisque la septième est majeure sur l'accord de tonique).
Par exemple, pour un blues en ''do'' :
* accord parfait de do majeur, C ({{Times New Roman|I}}<sup>er</sup> degré) ;
* accord parfait de fa majeur, F ({{Times New Roman|IV}}<sup>e</sup> degré) ;
* accord parfait de sol majeur, G ({{Times New Roman|V}}<sup>e</sup> degré).
Il existe quelques morceaux harmonisés avec des accords mineurs, comme par exemple ''As the Years Go Passing By'' d'Albert King (Duje Records, 1959).
La progression blues est organisée en trois blocs de quatre mesures ayant les fonctions suivantes (voir ci-dessus ''[[#Harmonie fonctionnelle|Harmonie fonctionnelle]]'') :
* quatre mesures toniques ;
* quatre mesures sous-dominantes ;
* quatre mesures dominantes.
La forme la plus simple, que Jeff Gardner appelle « forme A », est la suivante :
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, forme A
|-
! scope="row" | Tonique
| width="50px" | I
| width="50px" | I
| width="50px" | I
| width="50px" | I
|-
! scope="row" | Sous-domminante
| width="50px" | IV
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
! scope="row" | Dominante
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | V
|}
La progression {{Times New Roman|I-V}} des deux dernières mesures forment le ''{{lang|en|turnaround}}'', la demie cadence qui lance le cycle suivant. Nous présentons ci-dessous un exemple typique de ligne de basse ''({{lang|en|walking bass}})'' pour le ''{{lang|en|turnaround}}'' d'un blues en ''la'' :
[[Fichier:Turnaround classique blues en la.svg|Exemple typique de ligne de basse pour un ''turnaround'' de blues en ''la''.]]
[[Fichier:Blues mi harmonie elementaire.midi|thumb|Blues en ''mi'', harmonisé de manière élémentaire avec une ''{{lang|en|walking bass}}''.]]
Vous pouvez écouter ci-contre une harmonisation typique d'un blues en ''mi''. Les accords sont exécutés par une basse marchante ''({{lang|en|walking bass}})'', qui joue une arpège sur la triade avec l'ajout d'une sixte majeure et d'une septième mineure, et par une guitare qui joue un accord de puissance ''({{lang|en|power chord}})'', qui n'est composé que de la fondamentale et de la quinte juste, avec une sixte en appoggiature.
La forme B s'obtient en changeant la deuxième mesure : on joue un degré {{Times New Roman|IV}} au lieu d'un degré {{Times New Roman|I}}. La progression {{Times New Roman|I-IV}} sur les deux premières mesures est appelé ''{{lang|en|quick change}}''.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, forme B
|-
| width="50px" | I
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
| width="50px" | IV
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|-
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | V
|}
Par exemple, ''Sweet Home Chicago'' (Robert Johnson, 1936) est un blues en ''fa'' ; sa grille d'accords, aux variations près, suit une forme B :
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression de ''Sweet Home Chicago''
|-
| width="50px" | F
| width="50px" | B♭
| width="50px" | F
| width="50px" | F
|-
| width="50px" | B♭
| width="50px" | B♭
| width="50px" | F
| width="50px" | F
|-
| width="50px" | C7
| width="50px" | B♭7
| width="50px" | F7
| width="50px" | C7
|}
: Écouter {{lien web
| url =https://www.youtube.com/watch?v=dkftesK2dck
| titre = Robert Johnson "Sweet Home Chicago"
| auteur = Michal Angel
| site = YouTube
| date = 2007-12-09 | consulté le = 2020-12-17
}}
Les formes C et D s'obtiennent à partir des formes A et B en changeant le dernier accord par un accord sur le degré {{Times New Roman|I}}, ce qui forme une cadence plagale.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Progression blues, formes C et D
|-
| colspan="4" | …
|-
| colspan="4" | …
|-
| width="50px" | V
| width="50px" | IV
| width="50px" | I
| width="50px" | I
|}
L'harmonie peut être enrichie, notamment en jazz. Voici par exemple une grille du blues souvent utilisés en bebop.
{| class="wikitable" style="font-family:Times New Roman; text-align:center;"
|+ Exemple de progression de blues bebop sur une base de forme B
|-
| width="60px" | I<sup>7</sup>
| width="60px" | IV<sup>7</sup>
| width="60px" | I<sup>7</sup>
| width="60px" | V–<sup>7</sup> <nowiki>|</nowiki> I<sup>7</sup>
|-
| width="60px" | IV<sup>7</sup>
| width="60px" | IV<sup>7</sup>
| width="60px" | I<sup>7</sup>
| width="60px" | VI<sup>7 ♯9 ♭13</sup>
|-
| width="60px" | II–<sup>7</sup>
| width="60px" | V<sup>7</sup>
| width="60px" | V<sup>7</sup> <nowiki>|</nowiki> IV<sup>7</sup>
| width="60px" | II–<sup>7</sup> <nowiki>|</nowiki> V<sup>7</sup>
|}
On peut aussi trouver des blues sur huit mesures, sur seize mesures comme ''Watermelon Man'' de Herbie Hancock (album ''Takin' Off'', Blue Note, 1962) ou ''Let's Dance'' de Jim Lee (interprété par Chris Montez, Monogram, 1962)
* {{lien web
|url= https://www.dailymotion.com/video/x5iduwo
|titre=Herbie Hancock - Watermelon Man (1962)
|auteur=theUnforgettablesTv
|site=Dailymotion
|date=2003 |consulté le=2021-02-09
}}
* {{lien web
|url=https://www.youtube.com/watch?v=6JXshurYONc
|titre=Let's Dance
|auteur=Chris Montez
|site=YouTube
|date=2016-08-06 |consulté le=2021-02-09
}}
À l'inverse, certains blues peuvent avoir une structure plus simple que les douze mesure ; par exemple ''Hoochie Coochie Man'' de Willie Dixon (interprété par Muddy Waters sous le titre ''Mannish Boy'', Chicago Blues, 1954) est construit sur un seul accord répété tout le long de la chanson.
* {{lien web
|url=https://www.dailymotion.com/video/x5iduwo
|titre=Muddy Waters - Hoochie Coochie Man
|auteur=Muddy Waters
|site=Dailymotion
|date=2012 | consulté le=2021-02-09
}}
=== Cadence andalouse ===
La cadence andalouse est une progression de quatre accords, descendant par mouvement conjoint :
* en mode de ''mi'' (mode phrygien) : {{Times New Roman|IV}} - {{Times New Roman|III}} - {{Times New Roman|II}} - {{Times New Roman|I}} ;<br />par exemple en ''mi'' phrygien : Am - G - F - E ; en ''do'' phrygien : Fm - E♭ - D♭ - C ;<br />on notera que le degré {{Times New Roman|III}} est diésé dans l'accord final (ou bécarre s'il est bémol dans la tonalité) ;
* en mode mineur : {{Times New Roman|I}} - {{Times New Roman|VII}} - {{Times New Roman|VI}} - {{Times New Roman|V}} ;<br />par exemple en ''la'' mineur : Am - G - F - E ; en ''do'' mineur : Cm - B♭ - A♭ - m ;<br />comme précédemment, on notera que le degré {{Times New Roman|VII}} est diésé dans l'accord final.
=== Progressions selon le cercle des quintes ===
[[Fichier:Cercle quintes degres tonalite majeure.svg|vignette|Cercle des quinte justes (parcouru dans le sens des aiguilles d'une montre) des degrés d'une tonalité majeure.]]
La progression {{Times New Roman|V-I}} est la cadence parfaite, mais on peut aussi l'employer au milieu d'un morceau. Cette progression étant courte, sa répétition crée de la lassitude ; on peut la compléter par d'autres accords séparés d'une quinte juste, en suivant le « cercle des quintes » : {{Times New Roman|I-V-IX}}, la neuvième étant enharmonique de la seconde, on obtient {{Times New Roman|I-V-II}}.
On peut continuer de décrire le cercle des quintes : {{Times New Roman|I-V-II-VI}}, on obtient l'anatole dans le désordre ; on peut à l'inverse étendre les quintes vers la gauche, {{Times New Roman|IV-I-V-II-VI}}.
En musique populaire, on trouve fréquemment une progression fondée sur les accord {{Times New Roman|I}}, {{Times New Roman|IV}}, {{Times New Roman|V}} et {{Times New Roman|VI}}, popularisée dans les années 1950. La « progression années 1950 », « progression ''{{lang|en|fifties ('50)}}'' » ''({{lang|en|'50s progression}})'' est dans l'ordre {{Times New Roman|I-VI-IV-V}}. On trouve aussi cette progression en musique classique. Si la tonalité est majeure, la triade sur la sus-dominante est mineure, les autres sont majeures, on notera donc souvent {{Times New Roman|I-vi-IV-V}}. On peut avoir des permutations circulaires (le dernier accord venant au début, ou vice-versa) : {{Times New Roman|vi-IV-V-I}}, {{Times New Roman|IV-V-I-vi}} et {{Times New Roman|V-I-vi-IV}}.
{| class="wikitable"
|+ Accords selon la tonalité
! scope="col" | Tonalité
! scope="col" | Armure
! scope="col" style="font-family:Times New Roman" | I
! scope="col" style="font-family:Times New Roman" | IV
! scope="col" style="font-family:Times New Roman" | V
! scope="col" style="font-family:Times New Roman" | vi
|-
|''Do'' majeur || || C || F || G || Am
|-
|''Sol'' majeur || ''fa''♯ || G || C || D || Em
|-
|''Ré'' majeur || ''fa''♯, ''do''♯ || D || G || A || Bm
|-
|''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A || D || E || F♯m
|-
| ''Fa'' majeur || ''si''♭ || F || B♭ || C || Dm
|-
| ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭ || E♭ || F || Gm
|-
| ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭ || A♭ || B♭ || Cm
|}
Par exemple, en tonalité de ''do'' majeur, la progression {{Times New Roman|I-vi-IV-V}} sera C-Am-F-G.
Il existe d'autres progressions utilisant ces accords mais dans un autre ordre, typiquement {{Times New Roman|I–IV–vi–V}} ou une de ses permutations circulaires : {{Times New Roman|IV–vi–V-I}}, {{Times New Roman|vi–V-I-IV}} ou {{Times New Roman|V-I-IV-vi}}. Ou dans un autre ordre.
PV Nova l'illustre dans plusieurs de ses « expériences » dans la version {{Times New Roman|vi-V-IV-I}}, soit Am-G-F-C, ou encore {{Times New Roman|vi-IV-I-V}}, soit Am-F-C-G :
: {{lien web
| url = https://www.youtube.com/watch?v=w08LeZGbXq4
| titre = Expérience n<sup>o</sup> 6 — La Happy Pop
| auteur = PV Nova
| site = YouTube
| date = 2011-08-20 | consulté le = 2020-12-13
}}
et cela devient un gag récurrent avec son « chapeau des accords magiques qu'on nous ressort à toutes les sauces »
: {{lien web
| url = https://www.youtube.com/watch?v=VMY_vc4nZAU
| titre = Expérience n<sup>o</sup> 14 — La Soupe dou Brasil
| auteur = PV Nova
| site = YouTube
| date = 2012-10-03 | consulté le = 2020-12-17
}}
Cette récurrence est également parodiée par le groupe The Axis of Awesome avec ses « chansons à quatre accords » ''({{lang|en|four-chords song}})'', dans une sketch où ils mêlent 47 chansons en utilisant l'ordre {{Times New Roman|I-V-vi-IV}} :
: {{lien web
| url = https://www.youtube.com/watch?v=oOlDewpCfZQ
| titre = 4 Chords | Music Videos | The Axis Of Awesome
| auteur = The Axis of Awesome
| site = YouTube
| date = 2011-07-20 | consulté le = 2020-12-17
}}
{{boîte déroulante/début|titre=Chansons mêlées dans le sketch}}
# Journey : ''Don't Stop Believing'' ;
# James Blunt : ''You're Beautiful'' ;
# Black Eyed Peas : ''Where Is the Love'' ;
# Alphaville : ''Forever Young'' ;
# Jason Mraz : ''I'm Yours'' ;
# Train : ''Hey Soul Sister'' ;
# The Calling : ''Wherever You Will Go'' ;
# Elton John : ''Can You Feel The Love Tonight'' (''Le Roi lion'') ;
# Akon : ''Don't Matter'' ;
# John Denver : ''Take Me Home, Country Roads'' ;
# Lady Gaga : ''Paparazzi'' ;
# U2 : ''With Or Without You'' ;
# The Last Goodnight : ''Pictures of You'' ;
# Maroon Five : ''She Will Be Loved'' ;
# The Beatles : ''Let It Be'' ;
# Bob Marley : ''No Woman No Cry'' ;
# Marcy Playground : ''Sex and Candy'' ;
# Men At Work : ''Land Down Under'' ;
# thème de ''America's Funniest Home Videos'' (équivalent des émissions ''Vidéo Gag'' et ''Drôle de vidéo'') ;
# Jack Johnson : ''Taylor'' ;
# Spice Girls : ''Two Become One'' ;
# A Ha : ''Take On Me'' ;
# Green Day : ''When I Come Around'' ;
# Eagle Eye Cherry : ''Save Tonight'' ;
# Toto : ''Africa'' ;
# Beyonce : ''If I Were A Boy'' ;
# Kelly Clarkson : ''Behind These Hazel Eyes'' ;
# Jason DeRulo : ''In My Head'' ;
# The Smashing Pumpkins : ''Bullet With Butterfly Wings'' ;
# Joan Osborne : ''One Of Us'' ;
# Avril Lavigne : ''Complicated'' ;
# The Offspring : ''Self Esteem'' ;
# The Offspring : ''You're Gonna Go Far Kid'' ;
# Akon : ''Beautiful'' ;
# Timberland featuring OneRepublic : ''Apologize'' ;
# Eminem featuring Rihanna : ''Love the Way You Lie'' ;
# Bon Jovi : ''It's My Life'' ;
# Lady Gaga : ''Pokerface'' ;
# Aqua : ''Barbie Girl'' ;
# Red Hot Chili Peppers : ''Otherside'' ;
# The Gregory Brothers : ''Double Rainbow'' ;
# MGMT : ''Kids'' ;
# Andrea Bocelli : ''Time To Say Goodbye'' ;
# Robert Burns : ''Auld Lang Syne'' ;
# Five for fighting : ''Superman'' ;
# The Axis of Awesome : ''Birdplane'' ;
# Missy Higgins : ''Scar''.
{{boîte déroulante/fin}}
Vous pouvez par exemple jouer les accords C-G-Am-F ({{Times New Roman|I-V-vi-IV}}) et chanter dessus ''{{lang|en|Let It Be}}'' (Paul McCartney, The Beattles, 1970) ou ''Libérée, délivrée'' (Robert Lopez, ''La Reine des neiges'', 2013).
La progression {{Times New Roman|I-V-vi-IV}} est considérée comme « optimiste » tandis que sa variante {{Times New Roman|iv-IV-I-V}} est considérée comme « pessimiste ».
On peut voir la progression {{Times New Roman|I-vi-IV-V}} comme une variante de l'anatole {{Times New Roman|I-vi-ii-V}}, obtenue en remplaçant l'accord de sustonique {{Times New Roman|ii}} par l'accord de sous-dominante {{Times New Roman|IV}} (son relatif majeur, et degré ayant la même fonction).
==== Exemples de progression selon le cercle des quintes en musique classique ====
[[Fichier:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.midi|vignette|Dietrich Buxtehude, Psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]]
Cette progression selon la cercle des quintes, sous la forme {{Times New Roman|I-vi-IV-V}}, apparaît déjà au {{pc|xvii}}<sup>e</sup> siècle dans le psaume 42 ''Quem ad modum desiderat cervis'' (BuxVW92) de Dietrich Buxtehude (1637-1707). Le morceau est en ''fa'' majeur, la progression d'accords est donc F-Dm-B♭-C.
: {{lien web
| url = https://www.youtube.com/watch?v=8FmV9l1RqSg
| titre = D. Buxtehude - Quemadmodum desiderat cervus, BuxWV 92
| auteur = Longobardo
| site = YouTube
| date = 2013-04-06 | consulté la = 2021-01-01
}}
[[File:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.svg|vignette|450x450px|center|Dietrich Buxtehude, psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]]
{{clear}}
[[Fichier:JSBach BWV140 cantate 4 mesures.midi|vignette|J.-S. Bach, cantate BWV140, quatre premières mesures.]]
On la trouve également dans l'ouverture de la cantate ''{{lang|de|Wachet auf, ruft uns die Stimme}}'' de Jean-Sébastien Bach (BWV140, 1731). Le morceau est en ''mi''♭ majeur, la progression d'accords est donc E♭-Cm-A♭<sup>6</sup>-B♭.
[[Fichier:JSBach BWV140 cantate 4 mesures.svg|vignette|center|J.-S. Bach, cantate BWV140, quatre premières mesures.|alt=|517x517px]]
{{clear}}
[[Fichier:Mozart K310 Sonate8 mesures 45 a 49.midi|vignette|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]]
La même progression est utilisée par Mozart, par exemple dans le premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778), la progression d'accords est C-Am-F-G qui correspond à la progression {{Times New Roman|III-i-VI-VII}} de ''la'' mineur, mais à la progression {{Times New Roman|I-vi-IV-V}} de la gamme relative, ''do'' majeur .
[[Fichier:Mozart K310 Sonate8 mesures 45 a 49.svg|vignette|center|500px|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]]
=== Substitution tritonique ===
Un des accords les plus utilisés est donc l'accord de septième de dominante, {{Times New Roman|V<sup>7</sup><sub>+</sub>}} qui contient les degrés {{Times New Roman|V}}, {{Times New Roman|VII}}, {{Times New Roman|II}} ({{Times New Roman|IX}}) et {{Times New Roman|IV}}({{Times New Roman|XI}}) ; par exemple, en tonalité de ''do'' majeur, l'accord de ''sol'' septième (G<sup>7</sup>) contient les notes ''sol''-''si''-''ré''-''fa''. Si l'on prend l'accord dont la fondamentale est trois tons (triton) au-dessus ou en dessous — l'octave contenant six tons, on arrive sur la même note —, {{Times New Roman|♭II<sup>7</sup>}}, ici ''ré''♭ septième (D♭<sup>7</sup>), celui-ci contient les notes ''ré''♭-''fa''-''la''♭-''do''♭, cette dernière note étant l'enharmonique de ''si''. Les deux accords G<sup>7</sup> et D♭<sup>7</sup> ont donc deux notes en commun : le ''fa'' et le ''si''/''do''♭.
Il est donc fréquent en jazz de substituer l'accord {{Times New Roman|V<sup>7</sup><sub>+</sub>}} par l'accord {{Times New Roman|♭II<sup>7</sup>}}. Par exemple, la progression {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|V<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}} devient {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|♭II<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}}. C'est un procédé courant de réharmonisation (le fait de remplacer un accord par un autre dans un morceau existant).
Les six substitutions possibles sont donc : C<sup>7</sup>↔F♯<sup>7</sup> - D♭<sup>7</sup>↔G<sup>7</sup> - D<sup>7</sup>↔A♭<sup>7</sup> - E♭<sup>7</sup>↔A<sup>7</sup> - E<sup>7</sup>↔B♭<sup>7</sup> - F<sup>7</sup>↔B<sup>7</sup>.
[[Fichier:Übermäsiger Terzquartakkord.jpg|vignette|Exemple de cadence parfaite en ''do'' majeur avec substitution tritonique (sixte française).]]
Dans l'accord D♭<sup>7</sup>, si l'on remplace le ''do''♭ par son ''si'' enharmonique, on obtient un accord de sixte augmentée : ''ré''♭-''fa''-''la''♭-''si''. Cet accord est utilisé en musique classique depuis la Renaissance ; on distingue en fait trois accords de sixte augmentée :
* sixte française ''ré''♭-''fa''-''sol''-''si'' ;
* sixte allemande : ''ré''♭-''fa''-''la''♭-''si'' ;
* sixte italienne : ''ré''♭-''fa''-''si''.
Par exemple, le ''Quintuor en ''ut'' majeur'' de Franz Schubert (1828) se termine par une cadence parfaite dont l'accord de dominante est remplacé par une sixte française ''ré''♭-''fa''-''si''-''sol''-''si'' (''ré''♭ aux violoncelles, ''fa'' à l'alto, ''si''-''sol'' aux seconds violons et ''si'' au premier violon).
[[Fichier:Schubert C major Quintet ending.wav|vignette|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]]
[[Fichier:Schubert C major Quintet ending.png|vignette|center|upright=2.5|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]]
=== Autres accords de substitution ===
Substituer un accord consiste à utiliser un accord provenant d'une tonalité étrangère à la tonalité en cours. À la différence d'une modulation, la substitution est très courte et ne donne pas l'impression de changer de tonalité ; on a juste un sentiment « étrange » passager. Un court passage dans une autre tonalité est également appelée « emprunt ».
Nous avons déjà vu plusieurs méthodes de substitution :
* utilisation d'une note étrangère : une note étrangère — note de passage, appoggiature, anticipation, retard… — crée momentanément un accord hors tonalité ; en musique classique, ceci n'est pas considéré comme un accord en propre, mais en jazz, on parle « d'accord de passage » et « d'accord suspendu » ;
* utilisation d'une dominante secondaire : l'accord de dominante secondaire est hors tonalité ; le but ici est de faire une cadence parfaite, mais sur un autre degré que la tonique de la tonalité en cours ;
* la substitution tritonique, vue ci-dessus, pour remplacer un accord de septième de dominante.
Une dernière méthode consiste à remplacer un accord par un accord d'une gamme de même tonique, mais d'un autre mode ; on « emprunte » ''({{lang|en|borrow}})'' l'accord d'un autre mode. Par exemple, substituer un accord de la tonalité de ''do'' majeur par un accord de la tonalité de ''do'' mineur ou de ''do'' mode de ''mi'' (phrygien).
Donc en ''do'' majeur, on peut remplacer un accord de ''ré'' mineur septième (D<sub>m</sub><sup>7</sup>) par un accord de ''ré'' demi-diminué (D<sup>⌀</sup>, D<sub>m</sub><sup>7♭5</sup>) qui est un accord appartenant à la donalité de ''la'' mineur harmonique.
=== Forme AABA ===
La forme AABA est composée de deux progressions de huit mesures, notées A et B ; cela représente trente-deux mesures au total, on parle donc souvent en anglais de la ''{{lang|en|32-bars form}}''. C'est une forme que l'on retrouve dans de nombreuses chanson de comédies musicales de Broadway comme ''Have You Met Miss Jones'' (''{{lang|en|I'd Rather Be Right}}'', 1937), ''{{lang|en|Over the Rainbow}}'' (''Le Magicien d'Oz'', Harold Harlen, 1939), ''{{lang|en|All the Things You Are}}'' (''{{lang|en|Very Warm for may}}'', 1939).
Par exemple, la version de ''{{lang|en|Over the Rainbow}}'' chantée par Judy Garland est en ''la''♭ majeur et la progression d'accords est globalement :
* A (couplet) : A♭-Fm | Cm-A♭ | D♭ | Cm-A♭ | D♭ | D♭-F | B♭-E♭ | A♭
* B (pont) : A♭ | B♭m | Cm | D♭ | A♭ | B♭-G | Cm-G | B♭m-E♭
soit en degrés :
* A : {{Times New Roman|<nowiki>I-vi | iii-I | IV | iii-IV | IV | IV-vi | II-V | I</nowiki>}}
* B : {{Times New Roman|<nowiki>I | ii | iii | IV | I | II-VII | iii-VII | ii-V</nowiki>}}
Par rapport aux paroles de la chanson, on a
* A : couplet 1 ''« {{lang|en|Somewhere […] lullaby}} »'' ;
* A : couplet 2 ''« {{lang|en|Somewhere […] really do come true}} »'' ;
* B : pont ''« {{lang|en|Someday […] you'll find me}} »'' ;
* A : couplet 3 ''« {{lang|en|Somewhere […] oh why can't I?}} »'' ;
: {{lien web
| url = https://www.youtube.com/watch?v=1HRa4X07jdE
| titre = Judy Garland - Over The Rainbow (Subtitles)
| site = YouTube
| auteur = Overtherainbow
| consulté le = 2020-12-17
}}
Une mise en œuvre de la forme AABA couramment utilisée en jazz est la forme anatole (à le pas confondre avec la succession d'accords du même nom), en anglais ''{{lang|en|rythm changes}}'' car elle s'inspire du morceau ''{{lang|en|I Got the Rythm}}'' de George Gerschwin (''Girl Crazy'', 1930) :
* A : {{Times New Roman|I–vi–ii–V}} (succession d'accords « anatole ») ;
* B : {{Times New Roman|III<sup>7</sup>–VI<sup>7</sup>–II<sup>7</sup>–V<sup>7</sup>}} (les fondamentales forment une succession de quartes, donc parcourent le « cercle des quintes » à l'envers).
Par exemple, ''I Got the Rythm'' étant en ''ré''♭ majeur, la forme est :
* A : D♭ - B♭m - E♭m - A♭
* B : F7 - B♭7 - E♭7 - A♭7
=== Exemples ===
==== Début du Largo de la symphonie du Nouveau Monde ====
[[File:Largo nouveau monde 5 1res mesures.svg|vignette|Partition avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]]
[[File:Largo nouveau monde 5 1res mesures.midi|vignette|Fichier son avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]]
Nous avons reproduit ci-contre les cinq premières mesure du deuxième mouvement Largo de la symphonie « Du Nouveau Monde » (symphonie n<sup>o</sup> 9 d'Antonín Dvořák, 1893). Cliquez sur l'image pour l'agrandir.
Vous pouvez écouter cette partie jouée par un orchestre symphonique :
* {{lien web
|url =https://www.youtube.com/watch?v=y2Nw9r-F_yQ?t=565
|titre = Dvorak Symphony No.9 "From the New World" Karajan 1966
|site=YouTube (Seokjin Yoon)
|consulté le=2020-12-11
}} (à 9 min 25), par le Berliner Philharmoniker, dirigé par Herbert von Karajan (1966) ;
* {{lien web
|url = https://www.youtube.com/watch?v=ASlch7R1Zvo
|titre=Dvořák: Symphony №9, "From The New World" - II - Largo
|site=YouTube (diesillamusicae)
|consulté le=2020-12-11
}} : Wiener Philharmoniker, dirigé par Herbert von Karajan (1985).
{{clear}}
Cette partie fait intervenir onze instruments monodiques (ne jouant qu'une note à la fois) : des vents (trois bois, sept cuivres) et une percussion. Certains de ces instruments sont transpositeurs (les notes sur la partition ne sont pas les notes entendues). Jouées ensemble, ces onze lignes mélodiques forment des accords.
Pour étudier cette partition, nous réécrivons les parties des instruments transpositeurs en ''do'' et les parties en clef d’''ut'' en clef de ''fa''. Nous regroupons les parties en clef de ''fa'' d'un côté et les parties en clef de ''sol'' d'un autre.
{{boîte déroulante|Résultat|contenu=[[File:Largo nouveau monde 5 1res mesures transpositeurs en do.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde, en do.]]}}
Nous pouvons alors tout regrouper sous la forme d'un système de deux portées clef de ''fa'' et clef de ''sol'', comme une partition de piano.
{{boîte déroulante|Résultat|contenu=
[[Fichier:Largo nouveau monde 5 1res mesures accords.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords.]]
}}
[[Fichier:Largo nouveau monde 5 1res mesures accords.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords.]]
{{clear}}
Ensuite, nous ne gardons que la basse et les notes médium. Nous changeons éventuellement certaines notes d'octave afin de n'avoir que des superpositions de tierce ou de quinte (état fondamental des accords, en faisant ressortir les notes manquantes).
{{boîte déroulante|Résultat|contenu=
[[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords simplifiés.]]
}}
[[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords simplifiés.]]
Vous pouvez écouter cette partie jouée par un quintuor de cuivres (trompette, bugle, cor, trombone, tuba), donc avec des accords de cinq notes :
: {{lien web
|url=https://www.youtube.com/watch?v=pWfe60nbvjA
|titre = Largo from The New World Symphony by Dvorak
|site=YouTube (The Chamberlain Brass)
|consulté le=2020-12-11
}} : The American Academy of Arts & Letters in New York City (2017).
Nous allons maintenant chiffrer les accords.
Pour établir la basse chiffrée, il nous faut déterminer le parcours harmonique. Pour le premier accord, les tonalités les plus simples avec un ''sol'' dièse sont ''la'' majeur et ''fa'' dièse mineur ; comme le ''mi'' est bécarre, nous retenons ''la'' majeur, il s'agit donc d'un accord de quinte sur la dominante (les accords de dominante étant très utilisés, cela nous conforte dans notre choix). Puis nous avons un ''si'' bémol, nous pouvons être en ''fa'' majeur ou en ''ré'' mineur ; nous retenons ''fa'' majeur, c'est donc le renversement d'un accord sur le degré {{Times New Roman|II}}.
Dans la deuxième mesure, nous revenons en ''la'' majeur, puis, avec un ''la'' et un ''ré'' bémols, nous sommes en ''la'' bémol majeur ; nous avons donc un accord de neuvième incomplet sur la sensible, ou un accord de onzième incomplet sur la dominante.
Dans la troisième mesure, nous passons en ''ré'' majeur, avec un accord de dominante. Puis, nous arrivons dans la tonalité principale, avec le renversement d'un accord de dominante sans tierce suivi d'un accord de tonique. Nous avons donc une cadence parfaite, conclusion logique d'une phrase.
La progression des accords est donc :
{| class="wikitable"
! scope="row" | Tonalité
| ''la'' M - ''fa'' M || ''la'' M - ''la''♭ M || ''ré'' M - ''ré''♭ M || ''ré''♭ M
|-
! scope="row" | Accords
| {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|II}}<sup>6</sup><sub>4</sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|“V”}}<sup>9</sup><sub><s>5</s></sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|V}}<sup>+4</sup> || {{Times New Roman|I}}<sup>5</sup>
|}
Dans le chiffrage jazz, nous avons donc :
* une triade de ''mi'' majeur, E ;
* une triade de ''sol'' majeur avec un ''ré'' en basse : G/D ;
* à nouveau un E ;
* un accord de ''sol'' neuvième diminué incomplet, avec un ''ré'' bémol en basse : G dim<sup>9</sup>/D♭ ;
* un accord de ''la'' majeur, A ;
* un accord de ''la'' bémol septième avec une ''sol'' bémol à la basse : A♭<sup>7</sup>/G♭ ;
* la partie se conclue par un accord parfait de ''ré''♭ majeur, D♭.
Soit une progression E - G/D | E - G dim<sup>9</sup>/D♭ | A - A♭<sup>7</sup>/G♭ | D♭.
[[Fichier:Largo nouveau monde 5 1res mesures accords chiffres.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde en accords simplifiés.]]
{{clear}}
==== Thème de Smoke on the Water ====
Le morceau ''Smoke on the Water'' du groupe Deep Purple (album ''Machine Head'', 1972) possède un célèbre thème, un riff ''({{lang|en|rythmic figure}})'', joué à la guitare sous forme d'accords de puissance ''({{lang|en|power chords}})'', c'est-à-dire des accords sans tierce. Le morceau est en tonalité de ''sol'' mineur naturel (donc avec un ''fa''♮) avec ajout de la note bleue (''{{lang|en|blue note}}'', quinte diminuée, ''ré''♭), et les accords composant le thème sont G<sup>5</sup>, B♭<sup>5</sup>, C<sup>5</sup> et D♭<sup>5</sup>, ce dernier accord étant l'accord sur la note bleue et pouvant être considéré comme une appoggiature (indiqué entre parenthèse ci-après). On a donc ''a priori'', sur les deux premières mesures, une progression {{Times New Roman|I-III-IV}} puis {{Times New Roman|I-III-(♭V)-IV}}. Durant la majeure partie du thème, la guitare basse tient la note ''sol'' en pédale.
{{note|En jazz, la qualité « <sup>5</sup> » indique que l'on n'a que la quinte (et donc pas la tierce), contrairement à la notation de basse chiffrée.}}
: {{lien web
| url = https://www.dailymotion.com/video/x5ili04
| titre = Deep Purple — Smoke on the Water (Live at Montreux 2006)
| auteur = Deep Purple
| site = Dailymotion
| date = 2016 | consulté le = 2020-12-31
}}
Cependant, cette progression forme une mélodie, on peut donc plus la voir comme un contrepoint, la superposition de deux voies ayant un mouvement conjoint, joué par un seul instrument, la guitare, la voie 2 étant jouée une quarte juste en dessous de la voie 1 (la quarte juste descendante étant le renversement de la quinte juste ascendante) :
* voie 1 (aigu) : | ''sol'' - ''si''♭ - ''do'' | ''sol'' - ''si''♭ - (''ré''♭) - ''do'' | ;
* voie 2 (grave) : | ''ré'' - ''fa'' - ''sol'' | ''ré'' - ''fa'' - (''la''♭) - ''sol'' |.
En se basant sur la basse (''sol'' en pédale), nous pouvons considérer que ces deux mesures sont accompagnées d'un accord de Gm<sup>7</sup> (''sol''-''si''♭-''ré''-''fa''), chaque accord de la mélodie comprenant à chaque fois au moins une note de cet accord à l'exception de l'appogiature.
{| class="wikitable"
|+ Mise en évidence des notes de l'accord Gm<sup>7</sup>
|-
! scope="row" | Accords
| G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup>
|-
! scope="row" | Voie 1
| '''''sol''''' || '''''si''♭''' || ''do''
|-
! scope="row" | Voie 2
| '''''ré''''' || '''''fa''''' || '''''sol'''''
|-
! scope="row" | Basse
| '''''sol''''' || '''''sol''''' || '''''sol'''''
|}
Sur les deux mesures suivantes, la basse varie et suit les accords de la guitare avec un retard sur le dernier accord :
{| class="wikitable"
|+ Voies sur les mesure 3-4 du thème
|-
! scope="row" | Accords
| G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> || B♭<sup>5</sup> || G<sup>5</sup>
|-
! scope="row" | Voie 1
| ''sol'' || ''si''♭ || ''do'' || ''si''♭ || ''sol''
|-
! scope="row" | Voie 2
| ''ré'' || ''fa'' || ''sol'' || ''fa'' || ''ré''
|-
! scope="row" | Basse
| ''sol'' || ''sol'' || ''do'' || ''si''♭ || ''si''♭-''sol''
|}
Le couplet de cette chanson est aussi organisé sur une progression de quatre mesures, la guitare faisant des arpèges sur les accords G<sup>5</sup> (''sol''-''ré''-''sol'') et F<sup>5</sup> (''fa''-''do''-''fa'') :
: | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-F<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> |
soit une progression {{Times New Roman|<nowiki>| I-I | I-I | I-VII | I-I |</nowiki>}}. Nous pouvons aussi harmoniser le riff du thème sur cette progression, avec un accord F (''fa''-''la''-''do'') ; nous pouvons aussi nous rappeler que l'accord sur le degré {{Times New Roman|VII}} est plus volontiers considéré comme un accord de septième de dominante {{Times New Roman|V<sup>7</sup>}}, soit ici un accord Dm<sup>7</sup> (''ré''-''fa''-''la''-''do''). On peut donc considérer la progression harmonique sur le thème :
: | Gm-Gm | Gm-Gm | Gm-F ou Dm<sup>7</sup> | Gm-Gm |.
Cette analyse permet de proposer une harmonisation enrichie du morceau, tout en se rappelant qu'une des forces du morceau initial est justement la simplicité de sa structure, qui fait ressortir la virtuosité des musiciens. Nous pouvons ainsi comparer la version album à la version concert avec orchestre ou à la version latino de Pat Boone. À l'inverse, le groupe Psychostrip, dans une version grunge, a remplacé les accords par une ligne mélodique :
* le thème ne contient plus qu'une seule voie (la guitare ne joue pas des accords de puissance) ;
* dans les mesures 9 et 10, la deuxième guitare joue en contrepoint de type mouvement inverse, qui est en fait la voie 2 jouée en miroir ;
* l'arpège sur le couplet est remplacé par une ligne mélodique en ostinato sur une gamme blues.
{| class="wikitable"
|+ Contrepoint sur les mesures 9 et 10
|-
! scope="row" | Guitare 1
| ''sol'' ↗ ''si''♭ ↗ ''do''
|-
! scope="row" | Guitare 2
| ''sol'' ↘ ''fa'' ↘ ''ré''
|}
* {{lien web
| url = https://www.dailymotion.com/video/x5ik234
| titre = Deep Purple — Smoke on the Water (In Concert with the London Symphony Orchestra, 1999)
| auteur = Deep Purple
| site = Dailymotion
| date = 2016 | consulté le = 2020-12-31
}}
* {{lien web
| url = https://www.youtube.com/watch?v=MtUuNzVROIg
| titre = Pat Boone — Smoke on the Water (In a Metal Mood, No More Mr. Nice Guy, 1997)
| auteur = Orrore a 33 Giri
| site = YouTube
| date = 2019-06-24 | consulté le = 2020-12-31
}}
* {{lien web
| url = https://www.youtube.com/watch?v=n7zLlZ8B0Bk
| titre = Smoke on the Water (Heroes, 1993)
| auteur = Psychostrip
| site = YouTube
| date = 2018-06-20 | consulté le = 2020-12-31
}}
== Accords et improvisation ==
Nous avons vu précédemment (chapitre ''[[../Gammes et intervalles#Modes et improvisation|Gammes et intervalles > Modes et improvisation]]'') que le choix d'un mode adapté permet d'improviser sur un accord. L'harmonisation des gammes permet, en inversant le processus, d'étendre notre palette : il suffit de repérer l'accord sur une harmonisaiton de gamme, et d'utiliser cette gamme-là, dans le mode correspondant du degré de l'accord (voir ci-dessus ''[[#Harmonisation par des accords de septième|Harmonisation par des accords de septième]]'').
Par exemple, nous avons vu que l'accord sur le septième degré d'une gamme majeure était un accord demi-diminué ; nous savons donc que sur un accord demi-diminué, nous pouvons improviser sur le mode correspondant au septième degré, soit le mode de ''si'' (locrien).
Un accord de septième de dominante étant commun aux deux tonalités homonymes (par exemple ''fa'' majeur et ''fa'' mineur pour un ''do''<sup>7</sup><sub>+</sub> / C<sup>7</sup>), nous pouvons utiliser le mode de ''sol'' de la gamme majeure (mixolydien) ou de la gamme mineure mineure (mode phrygien dominant, ou phrygien espagnol) pour improviser. Mais l'accord de septième de dominante est aussi l'accord au début d'une grille blues ; on peut donc improviser avec une gamme blues, même si la tierce est majeure dans l'accord et mineure dans la gamme.
[[Fichier:Mode improvisation accords do complet.svg]]
== Autres accords courants ==
[[fichier:Cluster cdefg.png|vignette|Agrégat ''do - ré - mi - fa - sol''.]]
Nous avons vu précédemment l'harmonisation des tonalités majeures et mineures harmoniques par des triades et des accords de septième ; certains accords étant rarement utilisés (l'accord sur le degré {{Times New Roman|III}} et, pour les tonalités mineures harmoniques, l'accord sur la tonique), certains accords étant utilisés comme des accords sur un autre degré (les accords sur la sensible étant considérés comme des accords de dominante sans fondamentale).
Dans l'absolu, on peut utiliser n'importe quelle combinaison de notes, jusqu'aux agrégats, ou ''{{lang|en|clusters}}'' (mot anglais signifiant « amas », « grappe ») : un ensemble de notes contigües, séparées par des intervalles de seconde. Dans la pratique, on reste souvent sur des accords composés de superpositions de tierces, sauf dans le cas de transitions (voir la section ''[[#Notes étrangères|Notes étrangère]]'').
=== En musique classique ===
On utilise parfois des accords dont les notes ne sont pas dans la tonalité (hors modulation). Il peut s'agir d'accords de passage, de notes étrangères, par exemple utilisant un chromatisme (mouvement conjoint par demi-tons).
Outre les accords de passage, les autres accords que l'on rencontre couramment en musique classique sont les accords de neuvième, et les accords de onzième et treizième sur tonique. Ces accords sont simplement obtenus en continuant à empiler les tierces. Il n'y a pas d'accord d'ordre supérieur car la quinzième est deux octaves au-dessus de la fondamentale.
Comme pour les accords de septième, on distingue les accords de neuvième de dominante et les accords de neuvième d'espèce. Dans le cas de la neuvième de dominante, il y a une différence entre les tonalités majeures et mineures : l'intervalle de neuvième est respectivement majeur et mineur. Les chiffrages des renversements peuvent donc différer. Comme pour les accords de septième de dominante, on considère que les accords de septième sur le degré {{Times New Roman|VI}} sont en fait des accords de neuvième de dominante sans fondamentale.
Les accords de neuvième d'espèce sont en général préparés et résolus. Préparés : la neuvième étant une note dissonante (c'est à une octave près la seconde de la fondamentale), l'accord qui précède doit contenir cette note, mais dans un accord consonant ; la neuvième est donc commune avec l'accord précédent. Résolus : la dissonance est résolue en abaissant la neuvième par un mouvement conjoint. Par exemple, en tonalité de ''do'' majeur, si l'on veut utiliser un accord de neuvième d'espèce sur la tonique ''(do - mi - sol - si - ré)'', on peut utiliser avant un accord de dominante ''(sol - si - ré)'' en préparation puis un accord parfait sur le degré {{Times New Roman|IV}} ''(fa - la - do)'' en résolution ; nous avons donc sur la voie la plus aigüe la succession ''ré'' (consonant) - ''ré'' (dissonant) - ''do'' (consonant).
On rencontre également parfois des accords de onzième et de treizième. On omet en général la tierce, car elle est dissonante avec la onzième. L'accord le plus fréquemment rencontré est l'accord sur la tonique : on considère alors que c'est un accord sur la dominante que l'on a enrichi « par le bas », en ajoutant une quinte inférieure. par exemple, dans la tonalité de ''do'' majeur, l'accord ''do - sol - si - ré - fa'' est considéré comme un accord de septième de dominante sur tonique, le degré étant noté « {{Times New Roman|V}}/{{Times New Roman|I}} ». De même pour l'accord ''do - sol - si - ré - fa - la'' qui est considéré comme un accord de neuvième de dominante sur tonique.
=== En jazz ===
En jazz, on utilise fréquemment l'accord de sixte à la place de l'accord de septième majeure sur la tonique. Par exemple, en ''do'' majeur, on utilise l'accord C<sup>6</sup> ''(do - mi - sol - la)'' à la place de C<sup>Δ</sup> ''(do - mi - sol - si)''. On peut noter que C<sup>6</sup> est un renversement de Am<sup>7</sup> et pourrait donc se noter Am<sup>7</sup>/C ; cependant, le fait de le noter C<sup>6</sup> indique que l'on a bien un accord sur la tonique qui s'inscrit dans la tonalité de ''do'' majeur (et non, par exemple, de ''la'' mineur naturelle) — par rapport à l'harmonie fonctionnelle, on remarquera que Am<sup>7</sup> a une fonction tonique, l'utilisation d'un renversement de Am<sup>7</sup> à la place d'un accord de C<sup>Δ</sup> est donc logique.
Les accords de neuvième, onzième et treizième sont utilisés comme accords de septième enrichis. Le chiffrage suit les règles habituelles : on ajoute un « 9 », un « 11 » ou un « 13 » au chiffrage de l'accord de septième.
On utilise également des accords dits « suspendus » : ce sont des accords de transition qui sont obtenus en prenant une triade majeure ou mineure et en remplaçant la tierce par la quarte juste (cas le plus fréquent) ou la seconde majeure. Plus particulièrement, lorsque l'on parle simplement « d'accord suspendu » sans plus de précision, cela désigne l'accord de neuvième avec une quarte suspendue, noté « 9sus4 » ou simplement « sus ».
== L'harmonie tonale ==
L'harmonie tonale est un ensemble de règle assez strictes qui s'appliquent dans la musique savante européenne, de la période baroque à la période classique classique ({{pc|xiv}}<sup>e</sup>-{{pc|xviii}}<sup>e</sup> siècle). Certaines règles sont encore largement appliquées dans divers styles musicaux actuels, y compris populaire (rock, rap…), d'autres sont au contraire ignorées (par exemple, un enchaînement de plusieurs accords de même qualité forme un mouvement parallèle, ce qui est proscrit en harmonie tonale). De nos jours, on peut voir ces règles comme des règles « de bon goût », et leur application stricte comme une manière de composer « à la manière de ».
Précédemment, nous avons vu la progression des accords. Ci-après, nous abordons aussi la manière dont les notes de l'accord sont réparties entre plusieurs voix, et comment on construit chaque voix.
=== Concepts fondamentaux ===
; Consonance
: Les intervalles sont considérés comme « plus ou moins consonants » :
:* consonance parfaite : unisson, quinte et octave ;
:* consonance mixte (parfaite dans certains contextes, imparfaite dans d'autres) : quarte ;
:* consonance imparfaite : tierce et sixte ;
:* dissonance : seconde et septième.
; Degrés
: Certains degrés sont considérés comme « forts », « meilleurs », ce sont les « notes tonales » : {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (sous-dominante) et {{Times New Roman|V}} (dominante).
[[Fichier:Mouvements harmoniques.svg|vignette|upright=0.75|Mouvements harmoniques.]]
; Mouvements
: Le mouvement décrit la manière dont les voix évoluent les unes par rapport aux autres :
:# Mouvement parallèle : les voix sont séparées par un intervalle constant.
:# Mouvement oblique : une voix reste constante, c'est le bourdon ; l'autre monte ou descend.
:# Mouvement contraire : une voix descend, l'autre monte.
:# Échange de voix : les voix échangent de note ; les mélodies se croisent mais on a toujours le même intervalle harmonique.
{{clear}}
=== Premières règles ===
; Règle du plus court chemin
: Quand on passe d'un accord à l'autre, la répartition des notes se fait de sorte que chaque voix fait le plus petit mouvement possible. Notamment : si les deux accords ont des notes en commun, alors les voix concernées gardent la même note.
: Les deux voix les plus importantes sont la voix aigüe — soprano — et la voix la plus grave — basse. Ces deux voix sont relativement libres : la voix de soprano a la mélodie, la voix de basse fonde l'harmonie. La règle du plus court chemin s'applique surtout aux voix intermédiaires ; si l'on a des mouvements conjoints, ou du moins de petits intervalles — c'est le sens de la règle du plus court chemin —, alors les voix sont plus faciles à interpréter. Cette règle évite également que les voix n'empiètent l'une sur l'autre (voir la règle « éviter le croisement des voix »).
; Éviter les consonances parfaites consécutives
:* Lorsque deux voix sont à l'unisson ou à l'octave, elles ne doivent pas garder le même intervalle, l'effet serait trop plat.
:* Lorsque deux voix sont à la quarte ou à la quinte, elles ne doivent pas garder le même intervalle, car l'effet est trop dur.
: Pour éviter cela, lorsque l'on part d'un intervalle juste, on a intérêt à pratiquer un mouvement contraire aux voix qui ne gardent pas la même note, ou au moins un mouvement direct : les voix vont dans le même sens, mais l'intervalle change.
: Notez que même avec le mouvement contraire, on peut avoir des consonances parfaites consécutives, par exemple si une voix fait ''do'' aigu ↗ ''sol'' aigu et l'autre ''sol'' médium ↘ ''do'' grave.
: L'interdiction des consonances parfaites consécutives n'a pas été toujours appliquée, le mouvement parallèle strict a d'ailleurs été le premier procédé utilisé dans la musique religieuse au {{pc|x}}<sup>e</sup> siècle. On peut par exemple utiliser des quintes parallèles pour donner un style médiéval au morceau. On peut également utiliser des octaves parallèles sur plusieurs notes afin de créer un effet de renforcement de la mélodie.
: Par ailleurs, les consonances parfaites consécutives sont acceptées lorsqu'il s'agit d'une cadence (transition entre deux parties ou bien conclusion du morceau).
; Éviter le croisement des voix
: Les voix sont organisées de la plus grave à la plus aigüe. Deux voix n'étant pas à l'unisson, celle qui est plus aigüe ne doit pas devenir la plus grave et ''vice versa''.
; Soigner la partie soprano
: Comme c'est celle qu'on entend le mieux, c'est en général celle qui porte la mélodie principale. On lui applique des règles spécifiques :
:# Si elle chante la sensible dans un accord de dominante ({{Times New Roman|V}}), alors elle doit monter à la tonique, c'est-à-dire que la note suivante sera la tonique située un demi-ton au dessus.
:# Si l'on arrive à une quinte ou une octave entre les parties basse et soprano par un mouvement direct, alors sur la partie soprano, le mouvement doit être conjoint. On doit donc arriver à cette situation par des notes voisines au soprano.
; Préférer certains accords
: Les deux degrés les plus importants sont la tonique ({{Times New Roman|I}}) et la dominante ({{Times New Roman|V}}), les accords correspondants ont donc une importance particulière.
: À l'inverse, l'accord de sensible ({{Times New Roman|VII}}) n'est pas considéré comme ayant une fonction harmonique forte. On le considère comme un accord de dominante affaibli. En tonalité mineure, on évite également l'accord de médiante ({{Times New Roman|III}}).
: Donc on utilise en priorité les accords de :
:# {{Times New Roman|I}} et {{Times New Roman|V}}.
:# Puis {{Times New Roman|II}}, {{Times New Roman|IV}}, {{Times New Roman|VI}} ; et {{Times New Roman|III}} en mode majeur.
:# On évite {{Times New Roman|VII}} ; et {{Times New Roman|III}} en mode mineur.
; Préférer certains enchaînements
: Les enchaînements d'accord peuvent être classés par ordre de préférence. Par ordre de préférence décroissante (du « meilleur » au « moins bon ») :
:# Meilleurs enchaînements : quarte ascendante ou descendante. Notons que la quarte est le renversement de la quinte, on a donc des enchaînements stables et naturels, mais avec un intervalle plus court qu'un enchaînement de quintes.
:# Bons enchaînements : tierce ascendante ou descendante. Les accords consécutifs ont deux notes en commun.
:# Enchaînements médiocres : seconde ascendante ou descendante. Les accords sont voisins, mais ils n'ont aucune note en commun. On les utilise de préférence en mouvement ascendant, et on utilise surtout les enchaînements {{Times New Roman|IV}}-{{Times New Roman|V}}, {{Times New Roman|V}}-{{Times New Roman|VI}} et éventuellement {{Times New Roman|I}}-{{Times New Roman|II}}.
:# Les autres enchaînements sont à éviter.
: On peut atténuer l'effet d'un enchaînement médiocre en plaçant le second accord sur un temps faible ou bien en passant par un accord intermédiaire.
[[Fichier:Progression Vplus4 I6.svg|thumb|Résolution d'un accord de triton (quarte sensible) vers l'accord de sixte de la tonique.]]
; La septième descend par mouvement conjoint
: Dans un accord de septième de dominante, la septième — qui est donc le degré {{Times New Roman|IV}} — descend par mouvement conjoint — elle est donc suivie du degré {{Times New Roman|III}}.
: Corolaire : un accord {{Times New Roman|V}}<sup>+4</sup> se résout par un accord {{Times New Roman|I}}<sup>6</sup> : on a bien un enchaînement {{Times New Roman|V}} → {{Times New Roman|I}}, et la 7{{e}} (degré {{Times New Roman|IV}}), qui est la basse de l'accord {{Times New Roman|V}}<sup>+4</sup>, descend d'un degré pour donner la basse de l'accord {{Times New Roman|I}}<sup>6</sup> (degré {{Times New Roman|III}}).
{{clear}}
[[Fichier:Progression I64 V7plus I5.svg|thumb|Accord de sixte et de quarte cadentiel.]]
; Un accord de sixte et quarte est un accord de passage
: Le second renversement d'un accord parfait est soit une appoggiature, soit un accord de passage, soit un accord de broderie.
: S'il s'agit de l'accord de tonique {{Times New Roman|I}}<sup>6</sup><sub>4</sub>, c'est « accord de sixte et quarte de cadence », l'appoggiature de l'accord de dominante de la cadence parfaite.
{{clear}}
Mais il faut appliquer ces règles avec discernement. Par exemple, la voix la plus aigüe est celle qui s'entend le mieux, c'est donc elle qui porte la mélodie principale. Il est important qu'elle reste la plus aigüe. La voix la plus grave porte l'harmonie, elle pose les accords, il est donc également important qu'elle reste la plus grave. Ceci a deux conséquences :
# Ces deux voix extrêmes peuvent avoir des intervalles mélodiques importants et donc déroger à la règle du plus court chemin : la voix aigüe parce que la mélodie prime, la voix de basse parce que la progression d'accords prime.
# Les croisements des voix intermédiaires sont moins critiques.
Par ailleurs, si l'on applique strictement toutes les règles « meilleurs accords, meilleurs enchaînements », on produit un effet conventionnel, stéréotypé. Il est donc important d'utiliser les solutions « moins bonnes », « médiocres » pour apporter de la variété.
Ajoutons que les renversements d'accords permettent d'avoir plus de souplesse : on reste sur le même accord, mais on enrichit la mélodie sur chaque voix.
Le ''Bolero'' de Maurice Ravel (1928) brise un certain nombre de ces règles. Par exemple, de la mesure 39 à la mesure 59, la harpe joue des secondes. De la mesure 149 à la mesure 165, les piccolo jouent à la sixte, dans des mouvement strictement parallèle, ce qui donne d'ailleurs une sonorité étrange. À partir de la mesure 239, de nombreux instruments jouent en mouvement parallèles (piccolos, flûtes, hautbois, cor, clarinettes et violons).
=== Application ===
[[Fichier:Harmonisation possible de frere jacques exercice.svg|vignette|Exercice : harmoniser ''Frère Jacques''.]]
Harmoniser ''Frère Jacques''.
Nous considérons un morceau à quatre voix : basse, ténor, alto et soprano. La soprano chante la mélodie de ''Frère Jacques''. L'exercice consiste à proposer l'écriture des trois autres voix en respectant les règles énoncées ci-dessus. Pour simplifier, nous ajoutons les contraintes suivantes :
* toutes les voix chantent des blanches ;
* nous nous limitons aux accords de quinte (accords de trois sons composés d'une tierce et d'une quinte) sans avoir recours à leurs renversements (accords de sixte, accords de sixte et de quarte).
Les notes à gauche de la portée indiquent la tessiture (ou ambitus), l'amplitude que peut chanter la voix.
{{clear}}
{{boîte déroulante/début|titre=Solution possible}}
[[Fichier:Harmonisation possible de frere jacques solution.svg|vignette|Harmonisation possible de ''Frère Jacques'' (solution de l'exercice).]]
Il n'y a pas qu'une solution possible.
Le premier accord doit contenir un ''do''. Nous sommes manifestement en tonalité de ''do'' majeur, nous proposons de commencer par l'accord parfait de ''do'' majeur, I<sup>5</sup>.
Le deuxième accord doit comporter un ''ré''. Si nous utilisons l'accord de quinte de ''ré'', nous allons créer une quinte parallèle. Nous pourrions utiliser un renversement, mais nous nous imposons de chercher un autre accord. Il peut s'agir de l'accord ''si''<sup>5</sup> ''(si-ré-fa)'' ou de l'accord de ''sol''<sup>5</sup> ''(sol-si-ré)''. La dernière solution permet d'utiliser l'accord de dominante qui est un accord important de la tonalité. La règle du plus court chemin imposerait le ''sol'' grave pour la partie de basse, mais cela est proche de la limite du chanteur, nous préférons passer au ''sol'' aigu, plus facile à chanter. Nous vérifions qu'il n'y a pas de quinte parallèle : l'intervalle ascendant ''do-sol'' (basse-alto) devient ''sol-si'' (3<sup>ce</sup>), l'intervalle descendant ''do-sol'' (soprano-alto) devient ''ré-si'' (3<sup>ce</sup>).
De la même manière, pour le troisième accord, nous ne pouvons pas passer à un accord de ''la''<sup>5</sup> pour éviter une quinte parallèle. Nous avons le choix entre ''do''<sup>5</sup> ''(do-mi-sol)'' et ''mi''<sup>5</sup> ''(mi-sol-si)''. Nous préférons revenir à l'accord de fondamental, solution très stable (l'enchaînement {{Times New Roman|V}}-{{Times New Roman|I}} formant une cadence parfaite).
Pour le quatrième accord, nous pourrions rester sur l'accord parfait de ''do'' mais cela planterait en quelque sorte la fin du morceau puisque l'on resterait sur la cadence parfaite ; or, nous connaissons le morceau et savons qu'il n'est pas fini. Nous choisissons l'accord de ''la''<sup>5</sup> qui est une sixte ascendante ({{Times New Roman|I}}-{{Times New Roman|VI}}).
Nos aurions pu répartir les voix différemment. Par exemple :
* alto : ''sol''-''si''-''sol''-''do'' ;
* ténor : ''mi''-''ré''-''mi''-''mi''.
{{boîte déroulante/fin}}
[[Fichier:Harmonisation possible de frere jacques.midi|vignette|Fichier son correspondant.]]
{{clear}}
== Annexe ==
=== Accords en musique classique ===
Un accord est un ensemble de notes jouées simultanément. Il peut s'agir :
* de notes jouées par plusieurs instruments ;
* de notes jouées par un même instrument : piano, clavecin, orgue, guitare, harpe (la plupart des instruments à clavier et des instruments à corde).
Pour deux notes jouées simultanément, on parle d'intervalle « harmonique » (par opposition à l'intervalle « mélodique » qui concerne les notes jouées successivement).
Les notes répétées à différentes octaves ne changent pas la nature de l'accord.
La musique classique considère en général des empilements de tierces ; un accord de trois notes sera constitué de deux tierces successives, un accord de quatre notes de trois tierces…
Lorsque tous les intervalles sont des intervalles impairs — tierces, quintes, septièmes, neuvièmes, onzièmes, treizièmes… — alors l'accord est dit « à l'état fondamental » (ou encore « primitif » ou « direct »). La note de la plus grave est appelée « fondamentale » de l'accord. Lorsque l'accord comporte un ou des intervalles pairs, l'accord est dit « renversé » ; la note la plus grave est appelée « basse ».
De manière plus générale, l'accord est dit à l'état fondamental lorsque la basse est aussi la fondamentale. On a donc un état idéal de l'accord (état canonique) — un empilement strict de tierces — et l'état réel de l'accord — l'empilement des notes réellement jouées, avec d'éventuels redoublements, omissions et inversions ; et seule la basse indique si l'accord est à l'état fondamental ou renversé.
Le chiffrage dit de « basse continue » ''({{lang|it|basso continuo}})'' désigne la représentation d'un accord sous la forme d'un ou plusieurs chiffres arabes et éventuellement d'un chiffre romain.
==== Accords de trois notes ====
En musique classique, les seuls accords considérés comme parfaitement consonants, c'est-à-dire sonnant agréablement à l'oreille, sont appelés « accords parfaits ». Si l'on prend une tonalité et un mode donné, alors l'accord construit par superposition es degrés I, III et V de cette gamme porte le nom de la gamme qui l'a généré.
[[fichier:Accord do majeur chiffre.svg|vignette|upright=0.5|Accord parfait de ''do'' majeur chiffré.]]
Par exemple :
* « l'accord parfait de ''do'' majeur » est composé des notes ''do'', ''mi'' et ''sol'' ;
* « l'accord parfait de ''la'' mineur » est composé des notes ''la'', ''do'' et ''mi''.
Un accord parfait majeur est donc composé, en partant de la fondamentale, d'une tierce majeure et d'une quinte juste. Un accord parfait mineur est composé d'une tierce mineure et d'une quinte juste.
L'accord parfait à l'état fondamental est appelé « accord de quinte » et est simplement chiffré « 5 » pour indiquer la quinte.
On peut également commencer un accord sur sa deuxième ou sa troisième note, en faisant monter celle(s) qui précède(nt) à l'octave suivante. On parle alors de « renversement d'accord » ou d'accord « renversé ».
[[Fichier:Accord do majeur renversements chiffre.svg|vignette|upright=0.75|Accord parfait de ''do'' majeur et ses renversements, chiffrés.]]
Par exemple,
* le premier renversement de l'accord parfait de ''do'' majeur est :<br /> ''mi'', ''sol'', ''do'' ;
* le second renversement de l'accord parfait de do majeur est :<br /> ''sol'', ''do'', ''mi''.
Les notes conservent leur nom de « fondamentale », « tierce » et « quinte » malgré le changement d'ordre. La note la plus grave est appelée « basse ».
Dans le cas du premier renversement, le deuxième note est la tierce de la basse (la note la plus grave) et la troisième note est la sixte ; le chiffrage en chiffres arabes est donc « 6 » (puisque l'on omet la tierce) et l'accord est appelé « accord de sixte ». Pour le deuxième renversement, les intervalles sont la quarte et la sixte, le chiffrage est donc « 6-4 » et l'accord est appelé « accord de sixte et de quarte ».
Dans tous les cas, on chiffre le degré on considérant la fondamentale, par exemple {{Times New Roman|I}} si l'accord est construit sur la tonique de la gamme.
Les autres accords de trois notes que l'on rencontre sont :
* l'accord de quinte diminuée, constitué d'une tierce mineure et d'une quinte diminuée ; lorsqu'il est construit sur le septième degré d'une gamme, on considère que c'est un accord de septième de dominante sans fondamentale (voir plus bas), le degré est donc indiqué « “{{Times New Roman|V}}” » (cinq entre guillemets) et non « {{Times New Roman|VII}} » ;
* l'accord de quinte augmenté : il est composé d'une tierce majeure et qu'une quinte augmentée.
Dans le tableau ci-dessous,
* « m » désigne un intervalle mineur ;
* « M » un intervalle majeur ou le mode majeur ;
* « J » un intervalle juste ;
* « d » un intervalle diminué ;
* « A » un intervalle augmenté ;
* « mh » le mode mineur harmonique ;
* « ma » le mode mineur ascendant ;
* « md » le mode mineur descendant.
{| class="wikitable"
|+ Accords de trois notes
! scope="col" rowspan="2" | Nom
! scope="col" rowspan="2" | 3<sup>ce</sup>
! scope="col" rowspan="2" | 5<sup>te</sup>
! scope="col" rowspan="2" | État fondamental
! scope="col" rowspan="2" | 1<sup>er</sup> renversement
! scope="col" rowspan="2" | 2<sup>nd</sup> renversement
! scope="col" colspan="4"| Construit sur les degrés
|-
! scope="col" | M
! scope="col" | mh
! scope="col" | ma
! scope="col" | md
|-
| Accord parfait<br /> majeur || M || J
| accord de quinte || accord de sixte || accord de<br />sixte et de quarte
| {{Times New Roman|I, IV, V}} || {{Times New Roman|V, VI}} || {{Times New Roman|IV, V}} || {{Times New Roman|III, VI, VII}}
|-
| Accord parfait<br /> mineur || m || J
| accord de quinte || accord de sixte || accord de<br />sixte et de quarte
| {{Times New Roman|II, III, VI}} || {{Times New Roman|I, IV}} || {{Times New Roman|I, II}} || {{Times New Roman|I, IV, V}}
|-
| Accord de<br />quinte diminuée || m || d
| accord de<br />quinte diminuée || accord de<br />sixte sensible<br />sans fondamentale || accord de triton<br />sans fondamentale
| {{Times New Roman|VII (“V”)}} || {{Times New Roman|II, VII (“V”)}} || {{Times New Roman|VI, VII (“V”)}} || {{Times New Roman|II}}
|-
| Accord de<br />quinte augmentée || M || A
| accord de<br />quinte augmentée || accord de sixte<br />et de tierce sensible || accord de sixte et de quarte<br />sur sensible
| || {{Times New Roman|III}} || {{Times New Roman|III}} ||
|}
==== Accords de quatre notes ====
Les accords de quatre notes sont des accord composés de trois tierces superposées. La dernière note étant le septième degré de la gamme, on parle aussi d'accords de septième.
Ces accords sont dissonants : ils contiennent un intervalle de septième (soit une octave montante suivie d'une seconde descendante). Ils laissent donc une impression de « tension ».
Il existe sept différents types d'accords, ou « espèces ». Citons l'accord de septième de dominante, l'accord de septième mineure et l'accord de septième majeure.
===== L'accord de septième de dominante =====
[[Fichier:Accord 7e dominante do majeur renversements chiffre.svg|vignette|Accord de septième de dominante de ''do'' majeur et ses renversements, chiffrés.]]
L'accord de septième de dominante est l'empilement de trois tierces à partir de la dominante de la gamme, c'est-à-dire du {{Times New Roman|V}}<sup>e</sup> degré. Par exemple, l'accord de septième de dominante de ''do'' majeur est l'accord ''sol''-''si''-''ré''-''fa'', et l'accord de septième de dominante de ''la'' mineur est ''mi''-''sol''♯-''si''-''ré''. L'accord de septième de dominante dont la fondamentale est ''do'' (''do''-''mi''-''sol''-''si''♭) appartient à la gamme de ''fa'' majeur.
Que le mode soit majeur ou mineur, il est composé d'une tierce majeure, d'une quinte juste et d'une septième mineure (c'est un accord parfait majeur auquel on ajoute une septième mineure). C'est de loin l'accord de septième le plus utilisé ; il apparaît au {{pc|xvii}}<sup>e</sup> en musique classique.
Dans son état fondamental, son chiffrage est {{Times New Roman|V 7/+}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}). Le signe plus indique la sensible.
Son premier renversement est appelé « accord de quinte diminuée et sixte » et est noté {{Times New Roman|V 6/<s>5</s>}} (ou {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}).
Son deuxième renversement est appelé « accord de sixte sensible », puisque la sixte de l'accord est la sensible de la gamme, et est noté {{Times New Roman|V +6}} (ou {{Times New Roman|V<sup>+6</sup>}}).
Son troisième renversement est appelé « accord de quarte sensible » et est noté {{Times New Roman|V +4}} (ou {{Times New Roman|V<sup>+4</sup>}}).
[[Fichier:Accord 7e dominante sans fondamentale do majeur renversements chiffre.svg|vignette|Accord de septième de dominante sans fondamentale de ''do'' majeur et ses renversements, chiffrés.]]
On utilise aussi l'accord de septième de dominante sans fondamentale ; c'est alors un accord de trois notes.
Dans son état fondamental, c'est un « accord de quinte diminuée » placé sur le {{Times New Roman|VII}}<sup>e</sup> degré (mais c'est bien un accord construit sur le {{Times New Roman|V}}<sup>e</sup> degré), noté {{Times New Roman|“V” <s>5</s>}} (ou {{Times New Roman|“V”<sup><s>5</s></sup>}}). Notez les guillemets qui indiquent que la fondamentale V est absente.
Dans son premier renversement, c'est un « accord de sixte sensible sans fondamentale » noté {{Times New Roman|“V” +6/3}} (ou {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}}).
Dans son second renversement, c'est un « accord de triton sans fondamentale » (puisque le premier intervalle est une quarte augmentée qui comporte trois tons) noté {{Times New Roman|“V” 6/+4}} (ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}).
Notons qu'un accord de septième de dominante n'a pas toujours la dominante pour fondamentale : tout accord composé d'une tierce majeure, d'une quinte juste et d'une septième mineure est un accord de septième de dominante et est chiffré {{Times New Roman|<sup>7</sup><sub>+</sub>}}, quel que soit le degré sur lequel il est bâti (certaines notes peuvent avoir une altération accidentelle).
===== Les accords de septième d'espèce =====
Les autres accords de septièmes sont dits « d'espèce ».
L'accord de septième mineure est l'accord de septième formé sur la fondamentale d'une gamme mineure ''naturelle''. Par exemple, l'accord de septième mineure de ''la'' est ''la''-''do''-''mi''-''sol''. Il est composé d'une tierce mineure, d'une quinte juste et d'une septième mineure (c'est un accord parfait mineur auquel on ajoute une septième mineure).
L'accord de septième majeure est l'accord de septième formé sur la fondamentale d'une gamme majeure. Par exemple, L'accord de septième majeure de ''do'' est ''do''-''mi''-''sol''-''si''. Il est composé d'une tierce majeure, d'une quinte juste et d'une septième majeure (c'est un accord parfait majeur auquel on ajoute une septième majeure).
==== Utilisation du chiffrage ====
Le chiffrage est utilisé de deux manières.
La première manière, c'est la notation de la basse continue. La basse continue est une technique d'improvisation utilisée dans le baroque pour l'accompagnement d'instruments solistes. Sur la partition, on indique en général la note de basse de l'accord et le chiffrage en chiffres arabes.
La seconde manière, c'est pour l'analyse d'une partition. Le fait de chiffrer les accords permet de mieux en comprendre la structure.
De manière générale, on peut retenir que :
* le chiffrage « 5 » indique un accord parfait, superposition d'une tierce (majeure ou mineure) et d'une quinte juste ;
* le chiffrage « 6 » indique le premier renversement d'un accord parfait ;
* le chiffrage « 6/4 » indique le second renversement d'un accord parfait ;
* chiffrage « 7/+ » indique un accord de septième de dominante ;
* le signe « + » indique en général que la note de l'intervalle est la sensible ;
* un intervalle barré désigne un intervalle diminué.
[[fichier:Accords gamme do majeur la mineur.svg|class=transparent| center | Principaux accords construits sur les gammes de ''do'' majeur et de ''la'' mineur harmonique.]]
=== Notation « jazz » ===
En jazz et de manière générale en musique rock et populaire, la base d'un accord est la triade composée d'une tierce (majeure ou mineure) et d'une quinte juste. Pour désigner un accord, on utilise la note fondamentale, éventuellement désigné par une lettre dans le système anglo-saxon (A pour ''la'' etc.), suivi d'une qualité (comme « m », « + »…).
Les renversements ne sont pas notés de manière particulière, ils sont notés comme les formes fondamentales.
Dans les deux tableaux suivants, la fondamentale est notée X (remplace le C pour un accord de ''do'', le D pour un accord de ''ré''…). La construction des accords est décrite par la suite.
[[Fichier:Arbre accords triades 5d5J5A.svg|vignette|upright=1.5|Formation des triades présentée sous forme d'arbre.]]
{| class="wikitable"
|+ Notation des principales triades
|-
|
! scope="col" | Tierce<br />mineure (3m)
! scope="col" | Tierce<br />majeure (3M)
|-
! scope="row" | Quinte diminuée (5d)
| X<sup>o</sup>, Xm<sup>♭5</sup>, X–<sup>♭5</sup> ||
|-
! scope="row" | Quinte juste (5J)
| Xm, X– || X
|-
! scope="row" | Quinte augmentée (5A)
| || X+, X<sup>♯5</sup>
|}
[[Fichier:Triades do.svg|class=transparent|center|Triades de do.]]
{| class="wikitable"
|+ Notation des principaux accords de septième
|-
| colspan="2" |
! scope="col" | Tierce<br />mineure (3m)
! scope="col" | Tierce<br />majeure (3M)
|-
! scope="row" rowspan="2" | Quinte<br />diminuée (5d)
! scope="row" | Septième diminuée (7d)
| X<sup>o7</sup> ||
|-
! scope="row" | Septième mineure (7m)
| Xm<sup>7(♭5)</sup>, X–<sup>7(♭5)</sup>, X<sup>Ø</sup> ||
|-
! scope="row" rowspan="3" | Quinte<br />juste (5J)
! scope="row" | Sixte majeure (6M)
| Xm<sup>6</sup> || X<sup>6</sup>
|-
! scope="row" | Septième mineure (7m)
| Xm<sup>7</sup>, X–<sup>7</sup> || X<sup>7</sup>
|-
! scope="row" | Septième majeure (7M)
| Xm<sup>maj7</sup>, X–<sup>maj7</sup>, Xm<sup>Δ</sup>, X–<sup>Δ</sup> || X<sup>maj7</sup>, X<sup>Δ</sup>
|-
! scope="row" rowspan="2" | Quinte<br />augmentée (5A)
! scope="row" | Septième mineure (7m)
| || X+<sup>7</sup>
|-
! scope="row" | Septième majeure (7M)
| || X+<sup>maj7</sup>
|}
[[Fichier:Arbre accords septieme.svg|class=transparent|center|Formation des accords de septième présentée sous forme d'arbre.]]
[[Fichier:Accords do septieme.svg|class=transparent|center|Accord de do septième.]]
On notera que l'intervalle de sixte majeure est l'enharmonique de celui de septième diminuée (6M = 7d).
[[File:Principaux accords do.svg|class=transparent|center|Principaux accords de do.]]
==== Triades ====
; Accords fondés sur une tierce majeure
* accord parfait majeur : pas de notation
*: p. ex. « ''do'' » ou « C » pour l'accord parfait de ''do'' majeur (''do'' - ''mi'' - ''sol'')
; Accords fondés sur une tierce mineure
* accord parfait mineur : « m », « min » ou « – »
*: « ''do'' m », « ''do'' – », « Cm », « C– »… pour l'accord parfait de ''do'' mineur (''do'' - ''mi''♭ - ''sol'')
==== Triades modifiées ====
; Accords fondés sur une tierce majeure
* accord augmenté (la quinte est augmentée) : aug, +, ♯5
*: « ''do'' aug », « ''do'' + », « ''do''<sup>♯5</sup> » « Caug », « C+ » ou « C<sup>♯5</sup> » pour l'accord de ''do'' augmenté (''do'' - ''mi'' - ''sol''♯)
: L'accord augmenté est un empilement de tierces majeures. Ainsi, un accord augmenté a deux notes communes avec deux autres accords augmentés : C+ (''do'' - ''mi'' - ''sol''♯) a deux notes communes avec A♭+ (''la''♭ - ''do'' - ''mi'') et avec E+ (''mi'' - ''sol''♯ - ''si''♯) ; et on remarque que ces trois accords sont en fait enharmoniques (avec les enharmonies ''la''♭ = ''sol''♯ et ''si''♯ = ''do''). En effet, l'octave comporte six tons (sous la forme de cinq tons et deux demi-tons), et une tierce majeure comporte deux tons, on arrive donc à l'octave en ajoutant une tierce majeure à la dernière note de l'accord.
; Accords fondés sur une tierce mineure
* accord diminué (la quinte est diminuée) : dim, o, ♭5
*: « ''do'' dim », « ''do''<sup>o</sup> », « ''do''<sup>♭5</sup> », « Cdim », « C<sup>o</sup> » ou « C<sup>♭5</sup> » pour l'accord de ''do'' diminuné (''do'' - ''mi''♭ - ''sol''♭)
: On remarque que la quinte diminuée est l'enharmonique de la quarte augmentée et est l'intervalle appelé « triton » (car composé de trois tons).
; Accords fondés sur une tierce majeure ou mineure
* accord suspendu de seconde : la tierce est remplacée par une seconde majeure : sus2
*: « ''do''<sup>sus2</sup> » ou « C<sup>sus2</sup> » pour l'accord de ''do'' majeur suspendu de seconde (''do''-''ré''-''sol'')
* accord suspendu de quarte : la tierce est remplacée par une quarte juste : sus4
*: « ''do''<sup>sus4</sup> » ou « C<sup>sus4</sup> » pour l'accord de ''do'' majeur suspendu de quarte (''do''-''fa''-''sol'')
==== Triades appauvries ====
; Accords fondés sur une tierce majeure ou mineure
* accord de puissance : la tierce est omise, l'accord n'est constitué que de la fondamentale et de la quinte juste : 5
*: « ''do''<sup>5</sup> », « C<sup>5</sup> » pour l'accord de puissance de ''do'' (''do'' - ''la'')
{{note|Très utilisé dans les musiques rock, hard rock et heavy metal, il est souvent joué renversé (''la'' - ''do'') ou bien avec l'ajout de l'octave (''do'' - ''la'' - ''do'').}}
==== Triades enrichies ====
; Accords fondés sur une tierce majeure
* accord de septième (la 7<sup>e</sup> est mineure) : 7
*: « ''do''<sup>7</sup> », « C<sup>7</sup> » pour l'accord de ''do'' septième, appelé « accord de septième de dominante de ''fa'' majeur » en musique classique (''do'' - ''mi'' - ''sol'' - ''si''♭)
* accord de septième majeure : Δ, 7M ou maj7
*: « ''do'' <sup>Δ</sup> », « ''do'' <sup>maj7</sup> », « C<sup>Δ</sup> », « C<sup>7M</sup> »… pour l'accord de ''do'' septième majeure (''do'' - ''mi'' - ''sol'' - ''si'')
; Accords fondés sur une tierce mineure
* accord de mineur septième (la tierce et la 7<sup>e</sup> sont mineures) : m7, min7 ou –7
*: « ''do'' m<sup>7</sup> », « ''do'' –<sup>7</sup> », « Cm<sup>7</sup> », « C–<sup>7</sup> »… pour l'accord de ''do'' mineur septième, appelé « accord de septième de dominante de ''fa'' mineur » en musique classique (''do'' - ''mi''♭ - ''sol'' - ''si''♭)
* accord mineure septième majeure : m7M, m7maj, mΔ, –7M, –7maj, –Δ
*: « ''do'' m<sup>7M</sup> », « ''do'' m<sup>maj7</sup> », « ''do'' –<sup>Δ</sup> », « Cm<sup>7M</sup> », « Cm<sup>maj7</sup> », « C–<sup>Δ</sup> »… pour l'accord de ''do'' mineur septième majeure (''do'' - ''mi''♭ - ''sol'' - ''si'')
* accord de septième diminué (la quinte et la septième sont diminuée) : dim 7 ou o7
*: « ''do'' dim<sup>7</sup> », « ''do''<sup>o7</sup> », « Cdim<sup>7</sup> » ou « C<sup>o7</sup> » pour l'accord de ''do'' septième diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si''♭)
* accord demi-diminué (seule la quinte est diminuée, la septième est mineure) : Ø ou –7(♭5)
*: « ''do''<sup>Ø</sup> », « ''do''<sup>7(♭5)</sup> », « C<sup>Ø</sup> » ou « C<sup>7♭5</sup> » pour l'accord de ''do'' demi-diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si'')
=== Construction pythagoricienne des accords ===
Nous avons vu au débuts que lorsque l'on joue deux notes en même temps, leurs vibrations se superposent. Certaines superpositions créent un phénomène de battement désagréable, c'est le cas des secondes.
Dans le cas d'une tierce majeure, les fréquences des notes quadruple et quintuple d'une même base : les fréquences s'écrivent 4׃<sub>0</sub> et 5׃<sub>0</sub>. Cette superposition de vibrations est agréable à l'oreille. Nous avons également vu que dans le cas d'une quinte juste, les fréquences sont le double et le triple d'une même base, ou encore le quadruple et sextuple si l'on considère la moitié de cette base.
Ainsi, dans un accord parfait majeur, les fréquences des fondamentales des notes sont dans un rapport 4, 5, 6. De même, dans le cas d'un accord parfait mineur, les proportions sont de 1/6, 1/5 et 1/4.
{{voir|[[../Caractéristiques_et_notation_des_sons_musicaux#Construction_pythagoricienne_et_gamme_de_sept_tons|Caractéristiques et notation des sons musicaux > Construction pythagoricienne et gamme de sept tons]]}}
=== Un peu de physique : interférences ===
Les sons sont des vibrations. Lorsque l'on émet deux sons ou plus simultanément, les vibrations se superposent, on parle en physique « d'interférences ».
Le modèle le plus simple pour décrire une vibration est la [[w:fr:Fonction sinus|fonction sinus]] : la pression de l'air P varie en fonction du temps ''t'' (en secondes, s), et l'on a pour un son « pur » :
: P(''t'') ≈ sin(2π⋅ƒ⋅''t'')
où ƒ est la fréquence (en hertz, Hz) du son.
Si l'on émet deux sons de fréquence respective ƒ<sub>1</sub> et ƒ<sub>2</sub>, alors la pression vaut :
: P(''t'') ≈ sin(2π⋅ƒ<sub>1</sub>⋅''t'') + sin(2π⋅ƒ<sub>2</sub>⋅''t'').
Nous avons ici une [[w:fr:Identité trigonométrique#Transformation_de_sommes_en_produits,_ou_antilinéarisation|identité trigonométrique]] dite « antilinéarisation » :
: <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{f_1 + f_2}{2}t \right ) \cdot \sin \left ( 2\pi \frac{f_1 - f_2}{2}t \right ).</math>
On peut étudier simplement deux situations simples.
[[Fichier:Battements interferentiels.png|vignette|Deux sons de fréquences proches créent des battements : la superposition d'une fréquence et d'une enveloppe.]]
La première, c'est quand les fréquences ƒ<sub>1</sub> et ƒ<sub>2</sub> sont très proches. Alors, la moyenne (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2 est très proche de ƒ<sub>1</sub> et ƒ<sub>2</sub> ; et la demie différence (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2 est très proche de zéro. On a donc une enveloppe de fréquence très faible, (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2, dans laquelle s'inscrit un son de fréquence moyenne, (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2. C'est cette enveloppe de fréquence très faible qui crée les battements, désagréables à l'oreille.
Sur l'image ci-contre, le premier trait rouge montre un instant où les vibrations sont opposées ; elles s'annulent, le son s'éteint. Le second trait rouge montre un instant où les vibrations sont en phase : elle s'ajoutent, le son est au plus fort.
{{clear}}
La seconde, c'est lorsque les deux fréquences sont des multiples entiers d'une même fréquence fondamentale ƒ<sub>0</sub> : ƒ<sub>1</sub> = ''n''<sub>1</sub>⋅ƒ<sub>0</sub> et ƒ<sub>0</sub> = ''n''<sub>0</sub>⋅ƒ<sub>0</sub>. On a alors :
: <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{n_1 + n_2}{2}f_0 \cdot t \right ) \cdot \sin \left ( 2\pi \frac{n_1 - n_2}{2}f_0 \cdot t \right ).</math>
On multiplie donc deux fonctions qui ont des fréquences multiples de ƒ<sub>0</sub>. La différence minimale entre ''n''<sub>1</sub> et ''n''<sub>2</sub> vaut 1 ; on a donc une enveloppe dont la fréquence est au minimum la moitié de ƒ<sub>0</sub>, c'est-à-dire un son une octave en dessous de ƒ<sub>0</sub>. Donc, cette enveloppe ne crée pas d'effet de battement, ou plutôt, le battement est trop rapide pour être perçu comme tel. Dans cette enveloppe, on a une fonction sinus dont la fréquence est également un multiple de ƒ<sub>0</sub> ; l'enveloppe et la fonction qui y est inscrite ont donc de nombreux « points communs », d'où l'effet harmonieux.
=== Le tonnetz ===
[[File:Speculum musicae.png|thumb|right|225px|Euler, ''De harmoniæ veris principiis'', 1774, p. 350.]]
En allemand, le terme ''Tonnetz'' (se prononce « tône-netz ») signifie « réseau tonal ». C'est une représentation graphique des notes qui a été imaginée par [[w:Leonhard Euler|Leonhard Euler]] en 1739.
Cette représentation graphique peut aider à la mémorisation de certains concepts de l'harmonie. Cependant, son application est très limitée : elle ne concerne que l'intonation juste d'une part, et que les accords parfait des tonalités majeures et mineures naturelles d'autre part. La représentation contenant les douze notes de la musique savante occidentale, on peut bien sûr représenter d'autres objets, comme les accords de septième ou les accords diminués, mais la représentation graphique est alors compliquée et perd son intérêt pédagogique.
On part d'une note, par exemple le ''do''. Si on progresse vers la droite, on monte d'une quinte juste, donc ''sol'' ; vers la gauche, on descend d'une quinte juste, donc ''fa''. Si on va vers le bas, on monte d'une tierce majeure, donc ''mi'' ; si on va vers le haut, on descend d'une tierce majeure, donc ''la''♭ ou ''sol''♯
fa — do — sol — ré
| | | |
la — mi — si — fa♯
| | | |
do♯ — sol♯ — ré♯ — si♭
La figure forme donc un filet, un réseau. On voit que ce réseau « boucle » : si on descend depuis le ''do''♯, on monte d'une tierce majeure, on obtient un ''mi''♯ qui est l'enharmonique du ''fa'' qui est en haut de la colonne. Si on va vers la droite à partir du ''ré'', on obtient le ''la'' qui est au début de la ligne suivante.
Si on ajoute des diagonales allant vers la droite et le haut « / », on met en évidence des tierces mineures : ''la'' - ''do'', ''mi'' - ''sol'', ''si'' - ''ré'', ''do''♯ - ''mi''…
fa — do — sol — ré
| / | / | / |
la — mi — si — fa♯
| / | / | / |
do♯ — sol♯ — ré♯ — si♭
Donc les liens représentent :
* | : tierce majeure ;
* — : quinte juste ;
* / : tierce mineure.
[[Fichier:Tonnetz carre accords fr.svg|thumb|Tonnetz avec les accords parfaits. Les notes sont en notation italienne et les accords en notation jazz.]]
On met ainsi en évidence des triangles dont un côté est une quinte juste, un côté une tierce majeure et un côté une tierce mineure ; c'est-à-dire que les notes aux sommets du triangle forment un accord parfait majeur (par exemple ''do'' - ''mi'' - ''sol'') :
<div style="font-family:courier; background-color:#fafafa">
fa — '''do — sol''' — ré<br />
| / '''| /''' | / |<br />
la — '''mi''' — si — fa♯<br />
| / | / | / |<br />
do♯ — sol♯ — ré♯ — si♭
</div>
ou un accord parfait mineur (''la'' - ''do'' - ''mi'').
<div style="font-family:courier; background-color:#fafafa">
fa — '''do''' — sol — ré<br />
| '''/ |''' / | / |<br />
'''la — mi''' — si — fa♯<br />
| / | / | / |<br />
do♯ — sol♯ — ré♯ — si♭
</div>
Un triangle représente donc un accord, et un sommet représente une note. Si on passe d'un triangle à un triangle voisin, alors on passe d'un accord à un autre accord, les deux accords ayant deux notes en commun. Ceci illustre la notion de « plus court chemin » en harmonie : si on passe d'un accord à un autre en gardant un côté commun, alors on a un mouvement conjoint sur une seule des trois voix.
Par rapport à l'harmonie fonctionnelle : les accords sont contigus à leur fonction, par exemple en ''do'' majeur :
* fonction de tonique ({{Times New Roman|I}}) : C, A– et E– sont contigus ;
* fonction de sous-dominante ({{Times New Roman|IV}}) : F et D– sont contigus ;
* fonction de dominante ({{Times New Roman|V}}) : G et B<sup>o</sup> sont contigus.
On notera que les triangles d'un schéma ''tonnetz'' ne représentent que des accords parfaits. Pour représenter un accord de quinte diminuée (''si'' - ''ré'' - ''fa'') ou les accords de septième, en particulier l'accord de septième de dominante, il faut étendre le ''tonnetz'' et l'on obtient des figures différentes. Par ailleurs, il est adapté à ce que l'on appelle « l'intonation juste », puisque tous les intervalles sont idéaux.
[[Fichier:Tonnetz carre accords etendu fr.svg|vignette|Tonnetz étendu.]]
[[Fichier:Tonnetz carre do majeur accords fr.svg|vignette|Tonnetz de la tonalité de ''do'' majeur. La représentation de l'accord de quinte diminuée sur ''si'' (B<sup>o</sup>) est une ligne et non un triangle.]]
[[Fichier:Tonnetz carre do mineur accords fr.svg|vignette|Tonnetz des tonalités de ''do'' mineur naturel (haut) et ''do'' mineur harmonique (bas).]]
Si l'on étend un peu le réseau :
ré♭ — la♭ — mi♭ — si♭ — fa
| / | / | / | / |
fa — do — sol — ré — la
| / | / | / | / |
la — mi — si — fa♯ — do♯
| / | / | / | / |
do♯ — sol♯ — ré♯ — la♯ — mi♯
| / | / | / | / |
mi♯ — do — sol — ré — la
on peut donc trouver des chemins permettant de représenter les accords de septième de dominante (par exemple en ''do'' majeur, G<sup>7</sup>)
fa
/
sol — ré
| /
si
et des accords de quinte diminuée (en ''do'' majeur : B<sup>o</sup>)
fa
/
ré
/
si
Une gamme majeure ou mineure naturelle peut se représenter par un trapèze rectangle : ''do'' majeur
fa — do — sol — ré
| /
la — mi — si
et ''do'' mineur
la♭ — mi♭ — si♭
/ |
fa — do — sol — ré
En revanche, la représentation d'une tonalité nécessite d'étendre le réseau afin de pouvoir faire figurer tous les accords, deux notes sont représentées deux fois. La représentation des tonalités mineures harmoniques prend une forme biscornue, ce qui nuit à l'intérêt pédagogique de la représentation.
[[Fichier:Neo-Riemannian Tonnetz.svg|vignette|upright=2|Tonnetz avec des triangles équilatéraux.]]
On peut réorganiser le schéma en décalant les lignes, afin d'avoir des triangles équilatéraux. Sur la figure ci-contre (en notation anglo-saxonne) :
* si on monte en allant vers la droite « / », on a une tierce mineure ;
* si on descend en allant vers la droite « \ », on a une tierce majeure ;
* les liens horizontaux « — » représentent toujours des quintes justes
* les triangles pointe en haut sont des accords parfaits mineurs ;
* les triangles pointe en bas sont des accords parfaits majeurs.
On a alors les accords de septième de dominante
F
/
G — D
\ /
B
et de quinte diminuée
F
/
D
/
B
les tonalités majeures
F — C — G — D
\ /
A — E — B
et les tonalités mineures naturelles
A♭ — E♭ — B♭
/ \
F — C — G — D
== Notes et références ==
{{références}}
== Voir aussi ==
=== Liens externes ===
{{wikipédia|Consonance (harmonie tonale)}}
{{wikipédia|Disposition de l'accord}}
{{wikisource|Petit Manuel d’harmonie}}
* {{lien web
| url = https://www.apprendrelesolfege.com/chiffrage-d-accords
| titre = Chiffrage d'accords (classique)
| site = Apprendrelesolfege.com
| consulté le = 2020-12-03
}}
* {{lien web
| url = https://www.coursd-harmonie.fr/introduction/introduction2_le_chiffrage_d_accords.php
| titre = Introduction II : Le chiffrage d'accords
| site = Cours d'harmonie.fr
| consulté le = 2021-12-14
}}
* {{lien web
| url=https://www.coursd-harmonie.fr/
| titre = Cours d'harmonie en ligne
| auteur = Jean-Baptiste Voinet
| site=coursd-harmonie.fr
| consulté le = 2021-12-20
}}
* {{lien web
| url=http://e-harmonie.e-monsite.com/
| titre = Cours d'harmonie classique en ligne
| auteur = Olivier Miquel
| site=e-harmonie
| consulté le = 2021-12-24
}}
* {{lien web
| url=https://fr.audiofanzine.com/theorie-musicale/editorial/dossiers/les-gammes-et-les-modes.html
| titre = Les bases de l’harmonie
| site = AudioFanzine
| date = 2013-07-23
| consulté le = 2024-01-12
}}
----
''[[../Mélodie|Mélodie]]'' < [[../|↑]] > ''[[../Représentation musicale|Représentation musicale]]''
[[Catégorie:Formation musicale (livre)|Harmonie]]
1hih5xq25f09u0o94hmzgl3aqz2u5xb
Fonctionnement d'un ordinateur/Les optimisations du chargement des instructions
0
79799
745759
745626
2025-07-02T15:52:55Z
Mewtow
31375
/* Le Fetch Directed Instruction Prefetching */
745759
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle.
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
dmz9utxs9200uopasa6hyhicny7q7iu
745761
745759
2025-07-02T15:57:30Z
Mewtow
31375
/* Les unités de prédiction couplées au cache d'instruction */
745761
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle.
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
bbrwja241tjvgbc3flux4m2c6s2mua4
745762
745761
2025-07-02T16:04:11Z
Mewtow
31375
/* Les unités de prédiction couplées au cache d'instruction */
745762
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
===Les processeurs AMD d'adrchitecture K5, K6, K7, K8 et K10===
Les processeurs AMD anciens, d'architecture K5 à K10, ajoutaient à chaque ligne de cache plusieurs '''sélecteurs de branchement'''. Ils utilisaient la technique du prédécodage, qui décodait partiellement les instructions lors de leur entrée dans le cache L1. Ils savaient donc où se trouvaient les instructions dans chaque ligne de cache. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache.
Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquiat : que le branchement n'est pas pris si elle vaut 00, que c'est une isntruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination étaient mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
6nlp1p6ucw8xpw1hh8sz2ui3z48eo3i
745763
745762
2025-07-02T16:13:50Z
Mewtow
31375
/* Les processeurs AMD d'adrchitecture K5, K6, K7, K8 et K10 */
745763
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
===Les processeurs AMD d'architecture K5, K6, K7, K8 et K10===
L'usage de sélecteurs de branchements se marie bien avec la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient plusieurs branchements, dont la position est identifiée par le prédécodage. La position en question est encodée par un nombre qui indique à quel octet commence le branchement : est-ce l'octet numéro 3, numéro 7, etc. Elle est mémorisée dans les bits de controle de la ligne de cache. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a un bit par branchement, qui indique si le branchement est pris ou non. A la rigueur, un second bit facultatif peut indiquer si le branchement est inconditionnel ou non. L'adresse de destination est quant à elle mémorisée quelque part, soit dans la ligne de cache, soit dans un cache séparé, mais elle est mémorisée.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage; qui peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut.
Cependant, les processeurs AMD anciens, d'architecture K5 à K10 n'utilisaient pas cet algorithme simple, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement.
Les processeurs mentionnés utilisaient la technique du prédécodage et savaient donc où se trouvaient les instructions dans chaque ligne de cache. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache.
Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait : que le branchement n'est pas pris si elle vaut 00, que c'est une isntruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
0hm7mb4to7hb2wv2b6b46xw6m3g12fy
745764
745763
2025-07-02T16:20:20Z
Mewtow
31375
/* Les processeurs AMD d'architecture K5, K6, K7, K8 et K10 */
745764
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
===Les processeurs AMD d'architecture K5, K6, K7, K8 et K10===
L'usage de sélecteurs de branchements se marie bien avec la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient plusieurs branchements, dont la position est identifiée par le prédécodage. La position en question est encodée par un nombre qui indique à quel octet commence le branchement : est-ce l'octet numéro 3, numéro 7, etc. Elle est mémorisée dans les bits de controle de la ligne de cache. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a un bit par branchement, qui indique si le branchement est pris ou non. A la rigueur, un second bit facultatif peut indiquer si le branchement est inconditionnel ou non. L'adresse de destination est quant à elle mémorisée quelque part, soit dans la ligne de cache, soit dans un cache séparé, mais elle est mémorisée. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut.
Cependant, les processeurs AMD anciens, d'architecture K5 à K10 n'utilisaient pas cet algorithme simple, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement.
Les processeurs mentionnés utilisaient la technique du prédécodage et savaient donc où se trouvaient les instructions dans chaque ligne de cache. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache.
Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait : que le branchement n'est pas pris si elle vaut 00, que c'est une isntruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
mkuexvn6z6i0rple8rcb6ve4hj8tn2i
745765
745764
2025-07-02T16:20:48Z
Mewtow
31375
/* Les processeurs AMD d'architecture K5, K6, K7, K8 et K10 */
745765
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
===Les processeurs AMD d'architecture K5, K6, K7, K8 et K10===
L'usage de sélecteurs de branchements se marie bien avec la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient plusieurs branchements, dont la position est identifiée par le prédécodage. La position en question est encodée par un nombre qui indique à quel octet commence le branchement : est-ce l'octet numéro 3, numéro 7, etc. Elle est mémorisée dans les bits de controle de la ligne de cache. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a un bit par branchement, qui indique si le branchement est pris ou non. A la rigueur, un second bit facultatif peut indiquer si le branchement est inconditionnel ou non. L'adresse de destination est quant à elle mémorisée quelque part, soit dans la ligne de cache, soit dans un cache séparé, mais elle est mémorisée.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels.
La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K5 à K10 n'utilisaient pas cet algorithme simple, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement.
Les processeurs mentionnés utilisaient la technique du prédécodage et savaient donc où se trouvaient les instructions dans chaque ligne de cache. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache.
Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait : que le branchement n'est pas pris si elle vaut 00, que c'est une isntruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
7ndnddme173hyo94wbp0tizlkmaeg7u
745766
745765
2025-07-02T16:20:57Z
Mewtow
31375
/* Les processeurs AMD d'architecture K5, K6, K7, K8 et K10 */
745766
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
===Les processeurs AMD d'architecture K5, K6, K7, K8 et K10===
L'usage de sélecteurs de branchements se marie bien avec la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient plusieurs branchements, dont la position est identifiée par le prédécodage. La position en question est encodée par un nombre qui indique à quel octet commence le branchement : est-ce l'octet numéro 3, numéro 7, etc. Elle est mémorisée dans les bits de controle de la ligne de cache. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a un bit par branchement, qui indique si le branchement est pris ou non. A la rigueur, un second bit facultatif peut indiquer si le branchement est inconditionnel ou non. L'adresse de destination est quant à elle mémorisée quelque part, soit dans la ligne de cache, soit dans un cache séparé, mais elle est mémorisée.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels.
La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K5 à K10 n'utilisaient pas cet algorithme simple, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. Les processeurs mentionnés utilisaient la technique du prédécodage et savaient donc où se trouvaient les instructions dans chaque ligne de cache. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache.
Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait : que le branchement n'est pas pris si elle vaut 00, que c'est une isntruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
ofzapm0pmpojluihpntf4ktgon01sv8
745767
745766
2025-07-02T16:29:36Z
Mewtow
31375
/* Les processeurs AMD d'architecture K5, K6, K7, K8 et K10 */
745767
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
===Les processeurs AMD d'architecture K5, K6, K7, K8 et K10===
L'usage de sélecteurs de branchements se marie bien avec la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient plusieurs branchements, dont la position est identifiée par le prédécodage. La position en question est encodée par un nombre qui indique à quel octet commence le branchement : est-ce l'octet numéro 3, numéro 7, etc. Elle est mémorisée dans les bits de controle de la ligne de cache. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non. Mais un second bit facultatif peut indiquer si le branchement est inconditionnel ou non, et un troisième bit facultatif indique si c'est une instruction de retour de fonction.
L'adresse de destination est quant à elle mémorisée quelque part, soit dans la ligne de cache, soit dans un cache séparé, mais elle est mémorisée. Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. En clair, ajouter le troisième bit facultatif fait qu'on peut économiser la mémorisation de l'adresse de destination pour les instructions de retour de fonction.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels.
La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K5 à K10 n'utilisaient pas cet algorithme simple, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. Les processeurs mentionnés utilisaient la technique du prédécodage et savaient donc où se trouvaient les instructions dans chaque ligne de cache. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache.
Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait : que le branchement n'est pas pris si elle vaut 00, que c'est une isntruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
fye9kbsd0i6fvd7qmsszwzdsy4wz0i9
745768
745767
2025-07-02T16:39:04Z
Mewtow
31375
/* Les processeurs AMD d'architecture K5, K6, K7, K8 et K10 */
745768
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
===Les processeurs AMD d'architecture K5, K6, K7, K8 et K10===
La technique que nous allons voir se marie bien avec la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
Prenons par exemple une ligne de cache de 16 octets, dans laquelle on a 4 branchements de 2 octets chacun.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Position des branchements
|-
| Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction || Instruction Instruction || Instruction || bgcolor="#FFFF00" | Branch 3 || bgcolor="#FFFF00" | Branch 3 || Instruction || Instruction || bgcolor="#FFFF00" | Branch 4 || bgcolor="#FFFF00" | Branch 4
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0 || 0 || 0 || 1 || 0 || 0 || 0 || 1 || 0
|}
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non. Mais un second bit facultatif peut indiquer si le branchement est inconditionnel ou non, et un troisième bit facultatif indique si c'est une instruction de retour de fonction.
L'adresse de destination est quant à elle mémorisée quelque part, soit dans la ligne de cache, soit dans un cache séparé, mais elle est mémorisée. Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. En clair, ajouter le troisième bit facultatif fait qu'on peut économiser la mémorisation de l'adresse de destination pour les instructions de retour de fonction.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels.
La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K5 à K10 n'utilisaient pas cet algorithme simple, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. Les processeurs mentionnés utilisaient la technique du prédécodage et savaient donc où se trouvaient les instructions dans chaque ligne de cache. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache.
Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait : que le branchement n'est pas pris si elle vaut 00, que c'est une isntruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
ifnz8zv1qdlbyrzs61bhapwe3qhymdz
745769
745768
2025-07-02T16:39:45Z
Mewtow
31375
/* Les processeurs AMD d'architecture K5, K6, K7, K8 et K10 */
745769
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
===Les processeurs AMD d'architecture K5, K6, K7, K8 et K10===
La technique que nous allons voir se marie bien avec la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Position des branchements
|-
| Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0
|}
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non. Mais un second bit facultatif peut indiquer si le branchement est inconditionnel ou non, et un troisième bit facultatif indique si c'est une instruction de retour de fonction.
L'adresse de destination est quant à elle mémorisée quelque part, soit dans la ligne de cache, soit dans un cache séparé, mais elle est mémorisée. Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. En clair, ajouter le troisième bit facultatif fait qu'on peut économiser la mémorisation de l'adresse de destination pour les instructions de retour de fonction.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels.
La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K5 à K10 n'utilisaient pas cet algorithme simple, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. Les processeurs mentionnés utilisaient la technique du prédécodage et savaient donc où se trouvaient les instructions dans chaque ligne de cache. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache.
Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait : que le branchement n'est pas pris si elle vaut 00, que c'est une isntruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
f2o5um3ktcsmpz95gsdboxyeowt0yqq
745770
745769
2025-07-02T16:40:22Z
Mewtow
31375
/* Les processeurs AMD d'architecture K5, K6, K7, K8 et K10 */
745770
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
===Les processeurs AMD d'architecture K5, K6, K7, K8 et K10===
La technique que nous allons voir se marie bien avec la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0
|}
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non. Mais un second bit facultatif peut indiquer si le branchement est inconditionnel ou non, et un troisième bit facultatif indique si c'est une instruction de retour de fonction.
L'adresse de destination est quant à elle mémorisée quelque part, soit dans la ligne de cache, soit dans un cache séparé, mais elle est mémorisée. Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. En clair, ajouter le troisième bit facultatif fait qu'on peut économiser la mémorisation de l'adresse de destination pour les instructions de retour de fonction.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels.
La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K5 à K10 n'utilisaient pas cet algorithme simple, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. Les processeurs mentionnés utilisaient la technique du prédécodage et savaient donc où se trouvaient les instructions dans chaque ligne de cache. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache.
Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait : que le branchement n'est pas pris si elle vaut 00, que c'est une isntruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
kixx0j7fao14g5xnw46zjgthg0lqisk
745771
745770
2025-07-02T16:47:14Z
Mewtow
31375
/* Les processeurs AMD d'architecture K5, K6, K7, K8 et K10 */
745771
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
===Les processeurs AMD d'architecture K5, K6, K7, K8 et K10===
La technique que nous allons voir se marie bien avec la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0
|}
Il est possible d'améliorer le tout en précisant quel est le type du branchement. Par exemple, on peut distinguer les branchements inconditionnel et conditionnels, ou encore les instruction de retour de fonction. L'intérêt n'est pas évident, mais c'est lié au fait que les branchements inconditionnels sont toujours pris, et que les retour de fonction ont une adresse de destination qui est prédite par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. Deux bits suffisent pour indiquer : si c'est un branchement conditionnel, inconditionnel, un retour de fonction, ou une instruction qui n'est pas un branchement.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Saut inconditionnel || bgcolor="#FFFF00" | Saut inconditionnel || Instruction || bgcolor="#A00000" | Branch cond || bgcolor="#A00000" | Branch cond || Instruction || bgcolor="#F0F000" | Retour de fonction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 00 || 01 || 00 || 00 || 10 || 00 || 00 || 11
|}
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non.
L'adresse de destination est quant à elle mémorisée quelque part, soit dans la ligne de cache, soit dans un cache séparé, mais elle est mémorisée. Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. En clair, ajouter le troisième bit facultatif fait qu'on peut économiser la mémorisation de l'adresse de destination pour les instructions de retour de fonction.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels.
La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K5 à K10 n'utilisaient pas cet algorithme simple, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. Les processeurs mentionnés utilisaient la technique du prédécodage et savaient donc où se trouvaient les instructions dans chaque ligne de cache. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache.
Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait : que le branchement n'est pas pris si elle vaut 00, que c'est une isntruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
khqmfu9cl7qldrlus4z8fw09eluf635
745772
745771
2025-07-02T16:47:39Z
Mewtow
31375
/* Les processeurs AMD d'architecture K5, K6, K7, K8 et K10 */
745772
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
===Les sélecteurs de branchement intégrés au cache L1===
La technique que nous allons voir se marie bien avec la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0
|}
Il est possible d'améliorer le tout en précisant quel est le type du branchement. Par exemple, on peut distinguer les branchements inconditionnel et conditionnels, ou encore les instruction de retour de fonction. L'intérêt n'est pas évident, mais c'est lié au fait que les branchements inconditionnels sont toujours pris, et que les retour de fonction ont une adresse de destination qui est prédite par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. Deux bits suffisent pour indiquer : si c'est un branchement conditionnel, inconditionnel, un retour de fonction, ou une instruction qui n'est pas un branchement.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Saut inconditionnel || bgcolor="#FFFF00" | Saut inconditionnel || Instruction || bgcolor="#A00000" | Branch cond || bgcolor="#A00000" | Branch cond || Instruction || bgcolor="#F0F000" | Retour de fonction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 00 || 01 || 00 || 00 || 10 || 00 || 00 || 11
|}
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non.
L'adresse de destination est quant à elle mémorisée quelque part, soit dans la ligne de cache, soit dans un cache séparé, mais elle est mémorisée. Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. En clair, ajouter le troisième bit facultatif fait qu'on peut économiser la mémorisation de l'adresse de destination pour les instructions de retour de fonction.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels.
La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K5 à K10 n'utilisaient pas cet algorithme simple, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. Les processeurs mentionnés utilisaient la technique du prédécodage et savaient donc où se trouvaient les instructions dans chaque ligne de cache. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache.
Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait : que le branchement n'est pas pris si elle vaut 00, que c'est une isntruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
b5vhvgc0dsxazyk37eyux9c2p6wtg6h
745773
745772
2025-07-02T16:47:58Z
Mewtow
31375
/* Les sélecteurs de branchement intégrés au cache L1 */
745773
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
===Les sélecteurs de branchement intégrés au cache L1===
La technique que nous allons est une amélioration de la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0
|}
Il est possible d'améliorer le tout en précisant quel est le type du branchement. Par exemple, on peut distinguer les branchements inconditionnel et conditionnels, ou encore les instruction de retour de fonction. L'intérêt n'est pas évident, mais c'est lié au fait que les branchements inconditionnels sont toujours pris, et que les retour de fonction ont une adresse de destination qui est prédite par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. Deux bits suffisent pour indiquer : si c'est un branchement conditionnel, inconditionnel, un retour de fonction, ou une instruction qui n'est pas un branchement.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Saut inconditionnel || bgcolor="#FFFF00" | Saut inconditionnel || Instruction || bgcolor="#A00000" | Branch cond || bgcolor="#A00000" | Branch cond || Instruction || bgcolor="#F0F000" | Retour de fonction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 00 || 01 || 00 || 00 || 10 || 00 || 00 || 11
|}
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non.
L'adresse de destination est quant à elle mémorisée quelque part, soit dans la ligne de cache, soit dans un cache séparé, mais elle est mémorisée. Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. En clair, ajouter le troisième bit facultatif fait qu'on peut économiser la mémorisation de l'adresse de destination pour les instructions de retour de fonction.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels.
La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K5 à K10 n'utilisaient pas cet algorithme simple, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. Les processeurs mentionnés utilisaient la technique du prédécodage et savaient donc où se trouvaient les instructions dans chaque ligne de cache. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache.
Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait : que le branchement n'est pas pris si elle vaut 00, que c'est une isntruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
2cenmvl7eqajtfn36weg4gml85equa9
745774
745773
2025-07-02T17:35:28Z
Mewtow
31375
/* Les sélecteurs de branchement intégrés au cache L1 */
745774
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
===Les sélecteurs de branchement intégrés au cache L1===
La technique que nous allons est une amélioration de la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient potentiellement plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0
|}
Il est possible d'améliorer le tout en précisant quel est le type du branchement. Par exemple, on peut distinguer les branchements inconditionnel et conditionnels, ou encore les instruction de retour de fonction. L'intérêt n'est pas évident, mais c'est lié au fait que les branchements inconditionnels sont toujours pris, et que les retour de fonction ont une adresse de destination qui est prédite par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. Deux bits suffisent pour indiquer : si c'est un branchement conditionnel, inconditionnel, un retour de fonction, ou une instruction qui n'est pas un branchement.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Saut inconditionnel || bgcolor="#FFFF00" | Saut inconditionnel || Instruction || bgcolor="#A00000" | Branch cond || bgcolor="#A00000" | Branch cond || Instruction || bgcolor="#F0F000" | Retour de fonction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 00 || 01 || 00 || 00 || 10 || 00 || 00 || 11
|}
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non.
L'adresse de destination est quant à elle mémorisée quelque part, soit dans la ligne de cache, soit dans un cache séparé, mais elle est mémorisée. Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. En clair, ajouter le troisième bit facultatif fait qu'on peut économiser la mémorisation de l'adresse de destination pour les instructions de retour de fonction.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels.
La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K5 à K10 n'utilisaient pas cet algorithme simple, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. Les processeurs mentionnés utilisaient la technique du prédécodage et savaient donc où se trouvaient les instructions dans chaque ligne de cache. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache.
Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait : que le branchement n'est pas pris si elle vaut 00, que c'est une isntruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
6rzsyy0qwtq89chh7lwpktuw7c04vfr
745775
745774
2025-07-02T17:38:26Z
Mewtow
31375
/* Les sélecteurs de branchement intégrés au cache L1 */
745775
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
===Les sélecteurs de branchement intégrés au cache L1===
La technique que nous allons est une amélioration de la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient potentiellement plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0
|}
Il est possible d'améliorer le tout en précisant quel est le type du branchement. Par exemple, on peut distinguer les branchements inconditionnel et conditionnels, ou encore les instruction de retour de fonction. L'intérêt n'est pas évident, mais c'est lié au fait que les branchements inconditionnels sont toujours pris, et que les retour de fonction ont une adresse de destination qui est prédite par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. Deux bits suffisent pour indiquer : si c'est un branchement conditionnel, inconditionnel, un retour de fonction, ou une instruction qui n'est pas un branchement.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Saut inconditionnel || bgcolor="#FFFF00" | Saut inconditionnel || Instruction || bgcolor="#A00000" | Branch cond || bgcolor="#A00000" | Branch cond || Instruction || bgcolor="#F0F000" | Retour de fonction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 00 || 01 || 00 || 00 || 10 || 00 || 00 || 11
|}
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non.
L'adresse de destination des branchements détectés est quant à elle mémorisée quelque part, soit dans la ligne de cache, soit dans un cache séparé, mais elle est mémorisée. En général, les processeurs ne permettent que de supporter une seule adresse de destination. Par exemple, les processeurs AMD K5 ajoutaient, pour chaque ligne du cache d'instruction, une seule adresse de destination, qui était celle du premier branchement pris (conditionnel ou non).
Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. En clair, ajouter le troisième bit facultatif fait qu'on peut économiser la mémorisation de l'adresse de destination pour les instructions de retour de fonction.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels.
La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K5 à K10 n'utilisaient pas cet algorithme simple, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. Les processeurs mentionnés utilisaient la technique du prédécodage et savaient donc où se trouvaient les instructions dans chaque ligne de cache. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache.
Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait : que le branchement n'est pas pris si elle vaut 00, que c'est une isntruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
cbvquiux3lbgljfldmennk7cntekx5i
745776
745775
2025-07-02T17:39:11Z
Mewtow
31375
/* Les sélecteurs de branchement intégrés au cache L1 */
745776
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
===Les sélecteurs de branchement intégrés au cache L1===
La technique que nous allons est une amélioration de la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient potentiellement plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0
|}
Il est possible d'améliorer le tout en précisant quel est le type du branchement. Par exemple, on peut distinguer les branchements inconditionnel et conditionnels, ou encore les instruction de retour de fonction. L'intérêt n'est pas évident, mais c'est lié au fait que les branchements inconditionnels sont toujours pris, et que les retour de fonction ont une adresse de destination qui est prédite par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. Deux bits suffisent pour indiquer : si c'est un branchement conditionnel, inconditionnel, un retour de fonction, ou une instruction qui n'est pas un branchement.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Saut inconditionnel || bgcolor="#FFFF00" | Saut inconditionnel || Instruction || bgcolor="#A00000" | Branch cond || bgcolor="#A00000" | Branch cond || Instruction || bgcolor="#F0F000" | Retour de fonction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 00 || 01 || 00 || 00 || 10 || 00 || 00 || 11
|}
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non.
L'adresse de destination des branchements détectés est quant à elle mémorisée quelque part, soit dans la ligne de cache, soit dans un cache séparé, mais elle est mémorisée. Mémoriser l'adresse de destination dans la ligne de cache est de loin la solution la plus simple. En général, les processeurs ne supportent qu'une seule adresse de destination. Par exemple, les processeurs AMD K5 ajoutaient, pour chaque ligne du cache d'instruction, une seule adresse de destination, qui était celle du premier branchement pris (conditionnel ou non).
Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. En clair, ajouter le troisième bit facultatif fait qu'on peut économiser la mémorisation de l'adresse de destination pour les instructions de retour de fonction.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels.
La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K5 à K10 n'utilisaient pas cet algorithme simple, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. Les processeurs mentionnés utilisaient la technique du prédécodage et savaient donc où se trouvaient les instructions dans chaque ligne de cache. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache.
Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait : que le branchement n'est pas pris si elle vaut 00, que c'est une isntruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
nm0ore4ji470zdtph47ql6anexa9br8
745801
745776
2025-07-02T19:10:56Z
Mewtow
31375
/* Les unités de prédiction couplées au cache d'instruction */
745801
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle. Les informations en question peuvent être des adresses de destination, ou simplement de quoi déterminer si le branchement est pris ou non.
===L'incorporation du ''Branch Target Buffer'' dans le cache d'instruction===
Une première optimisation permet de se passer de ''Branch Target Buffer''. Pour rappel, celui-ci est un cache qui mémorise, pour chaque branchement, quelle est son adresse de destination. Il peut contenir d'autres informations de prédiction, mais laissons-les de côté pour le moment.
L'idée est de mémoriser les adresse de destination des branchements dans le cache d'instruction, dans les lignes de cache. 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. En général, les processeurs ne supportent qu'une seule adresse de destination. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémorisée. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela.
Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. En clair, ajouter le troisième bit facultatif fait qu'on peut économiser la mémorisation de l'adresse de destination pour les instructions de retour de fonction.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
===Les sélecteurs de branchement intégrés au cache L1===
La technique que nous allons est une amélioration de la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient potentiellement plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0
|}
Il est possible d'améliorer le tout en précisant quel est le type du branchement. Par exemple, on peut distinguer les branchements inconditionnel et conditionnels, ou encore les instruction de retour de fonction. L'intérêt n'est pas évident, mais c'est lié au fait que les branchements inconditionnels sont toujours pris, et que les retour de fonction ont une adresse de destination qui est prédite par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. Deux bits suffisent pour indiquer : si c'est un branchement conditionnel, inconditionnel, un retour de fonction, ou une instruction qui n'est pas un branchement.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Saut inconditionnel || bgcolor="#FFFF00" | Saut inconditionnel || Instruction || bgcolor="#A00000" | Branch cond || bgcolor="#A00000" | Branch cond || Instruction || bgcolor="#F0F000" | Retour de fonction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 00 || 01 || 00 || 00 || 10 || 00 || 00 || 11
|}
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels.
La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K5 à K10 n'utilisaient pas cet algorithme simple, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. Les processeurs mentionnés utilisaient la technique du prédécodage et savaient donc où se trouvaient les instructions dans chaque ligne de cache. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache.
Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait : que le branchement n'est pas pris si elle vaut 00, que c'est une isntruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
bl7z4ulmopvmjgyl33nrca99sel7zfn
745802
745801
2025-07-02T19:11:32Z
Mewtow
31375
/* Les unités de prédiction couplées au cache d'instruction */
745802
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle. Les informations en question peuvent être des adresses de destination, ou simplement de quoi déterminer si le branchement est pris ou non.
===L'incorporation du ''Branch Target Buffer'' dans le cache d'instruction===
Une première optimisation permet de se passer de ''Branch Target Buffer''. Pour rappel, celui-ci est un cache qui mémorise, pour chaque branchement, quelle est son adresse de destination. Il peut contenir d'autres informations de prédiction, mais laissons-les de côté pour le moment.
L'idée est de mémoriser les adresse de destination des branchements dans le cache d'instruction, dans les lignes de cache. 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. En général, les processeurs ne supportent qu'une seule adresse de destination. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémorisée. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela.
Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. En clair, ajouter le troisième bit facultatif fait qu'on peut économiser la mémorisation de l'adresse de destination pour les instructions de retour de fonction.
===Les sélecteurs de branchement intégrés au cache L1===
La technique que nous allons est une amélioration de la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient potentiellement plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0
|}
Il est possible d'améliorer le tout en précisant quel est le type du branchement. Par exemple, on peut distinguer les branchements inconditionnel et conditionnels, ou encore les instruction de retour de fonction. L'intérêt n'est pas évident, mais c'est lié au fait que les branchements inconditionnels sont toujours pris, et que les retour de fonction ont une adresse de destination qui est prédite par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. Deux bits suffisent pour indiquer : si c'est un branchement conditionnel, inconditionnel, un retour de fonction, ou une instruction qui n'est pas un branchement.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Saut inconditionnel || bgcolor="#FFFF00" | Saut inconditionnel || Instruction || bgcolor="#A00000" | Branch cond || bgcolor="#A00000" | Branch cond || Instruction || bgcolor="#F0F000" | Retour de fonction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 00 || 01 || 00 || 00 || 10 || 00 || 00 || 11
|}
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels.
La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K5 à K10 n'utilisaient pas cet algorithme simple, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. Les processeurs mentionnés utilisaient la technique du prédécodage et savaient donc où se trouvaient les instructions dans chaque ligne de cache. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache.
Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait : que le branchement n'est pas pris si elle vaut 00, que c'est une isntruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
82amub1he51bfdel16oittyxc7fuz3r
745803
745802
2025-07-02T19:12:52Z
Mewtow
31375
/* Les unités de prédiction couplées au cache d'instruction */
745803
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle. Les informations en question peuvent être des adresses de destination, ou simplement de quoi déterminer si le branchement est pris ou non.
===Les sélecteurs de branchement intégrés au cache L1===
La technique que nous allons est une amélioration de la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient potentiellement plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0
|}
Il est possible d'améliorer le tout en précisant quel est le type du branchement. Par exemple, on peut distinguer les branchements inconditionnel et conditionnels, ou encore les instruction de retour de fonction. L'intérêt n'est pas évident, mais c'est lié au fait que les branchements inconditionnels sont toujours pris, et que les retour de fonction ont une adresse de destination qui est prédite par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. Deux bits suffisent pour indiquer : si c'est un branchement conditionnel, inconditionnel, un retour de fonction, ou une instruction qui n'est pas un branchement.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Saut inconditionnel || bgcolor="#FFFF00" | Saut inconditionnel || Instruction || bgcolor="#A00000" | Branch cond || bgcolor="#A00000" | Branch cond || Instruction || bgcolor="#F0F000" | Retour de fonction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 00 || 01 || 00 || 00 || 10 || 00 || 00 || 11
|}
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels.
===L'incorporation du ''Branch Target Buffer'' dans le cache d'instruction===
Une première optimisation permet de se passer de ''Branch Target Buffer''. Pour rappel, celui-ci est un cache qui mémorise, pour chaque branchement, quelle est son adresse de destination. Il peut contenir d'autres informations de prédiction, mais laissons-les de côté pour le moment.
L'idée est de mémoriser les adresse de destination des branchements dans le cache d'instruction, dans les lignes de cache. 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. En général, les processeurs ne supportent qu'une seule adresse de destination. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémorisée. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela.
Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. Leurs adresses de destination n'ont pas forcément besoin d'être mémorisées dans les lignes de cache.
La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K6 à K10 n'utilisaient pas cette méthode, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. Les processeurs mentionnés utilisaient la technique du prédécodage et savaient donc où se trouvaient les instructions dans chaque ligne de cache. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache.
Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait : que le branchement n'est pas pris si elle vaut 00, que c'est une isntruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
baxhi2f1wkl0bxvbyjak8qi686t1l4x
745804
745803
2025-07-02T19:16:00Z
Mewtow
31375
/* L'incorporation du Branch Target Buffer dans le cache d'instruction */
745804
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle. Les informations en question peuvent être des adresses de destination, ou simplement de quoi déterminer si le branchement est pris ou non.
===Les sélecteurs de branchement intégrés au cache L1===
La technique que nous allons est une amélioration de la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient potentiellement plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0
|}
Il est possible d'améliorer le tout en précisant quel est le type du branchement. Par exemple, on peut distinguer les branchements inconditionnel et conditionnels, ou encore les instruction de retour de fonction. L'intérêt n'est pas évident, mais c'est lié au fait que les branchements inconditionnels sont toujours pris, et que les retour de fonction ont une adresse de destination qui est prédite par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. Deux bits suffisent pour indiquer : si c'est un branchement conditionnel, inconditionnel, un retour de fonction, ou une instruction qui n'est pas un branchement.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Saut inconditionnel || bgcolor="#FFFF00" | Saut inconditionnel || Instruction || bgcolor="#A00000" | Branch cond || bgcolor="#A00000" | Branch cond || Instruction || bgcolor="#F0F000" | Retour de fonction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 00 || 01 || 00 || 00 || 10 || 00 || 00 || 11
|}
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels.
===L'incorporation du ''Branch Target Buffer'' dans le cache d'instruction===
Une première optimisation permet de se passer de ''Branch Target Buffer''. Pour rappel, celui-ci est un cache qui mémorise, pour chaque branchement, quelle est son adresse de destination. Il peut contenir d'autres informations de prédiction, mais laissons-les de côté pour le moment.
L'idée est de déplacer les adresse de destination des branchements dans le cache d'instruction, dans les lignes de cache. 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. En général, les processeurs ne supportent qu'une seule adresse de destination. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémorisée. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela.
Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', séparée du ''Branch Target Buffer''. Leurs adresses de destination n'ont pas forcément besoin d'être mémorisées dans les lignes de cache.
La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K6 à K10 n'utilisaient pas cette méthode, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. Les processeurs mentionnés utilisaient la technique du prédécodage et savaient donc où se trouvaient les instructions dans chaque ligne de cache. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache.
Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait : que le branchement n'est pas pris si elle vaut 00, que c'est une instruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
pkkdsa1dyb0elcmq2dbuxueiqnx4mxa
745805
745804
2025-07-02T19:17:54Z
Mewtow
31375
/* L'incorporation du Branch Target Buffer dans le cache d'instruction */
745805
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle. Les informations en question peuvent être des adresses de destination, ou simplement de quoi déterminer si le branchement est pris ou non.
===Les sélecteurs de branchement intégrés au cache L1===
La technique que nous allons est une amélioration de la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient potentiellement plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0
|}
Il est possible d'améliorer le tout en précisant quel est le type du branchement. Par exemple, on peut distinguer les branchements inconditionnel et conditionnels, ou encore les instruction de retour de fonction. L'intérêt n'est pas évident, mais c'est lié au fait que les branchements inconditionnels sont toujours pris, et que les retour de fonction ont une adresse de destination qui est prédite par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. Deux bits suffisent pour indiquer : si c'est un branchement conditionnel, inconditionnel, un retour de fonction, ou une instruction qui n'est pas un branchement.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Saut inconditionnel || bgcolor="#FFFF00" | Saut inconditionnel || Instruction || bgcolor="#A00000" | Branch cond || bgcolor="#A00000" | Branch cond || Instruction || bgcolor="#F0F000" | Retour de fonction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 00 || 01 || 00 || 00 || 10 || 00 || 00 || 11
|}
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels.
===L'incorporation du ''Branch Target Buffer'' dans le cache d'instruction===
Une première optimisation permet de se passer de ''Branch Target Buffer''. Pour rappel, celui-ci est un cache qui mémorise, pour chaque branchement, quelle est son adresse de destination. Il peut contenir d'autres informations de prédiction, mais laissons-les de côté pour le moment.
L'idée est de déplacer les adresse de destination des branchements dans le cache d'instruction, dans les lignes de cache. 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. En général, les processeurs ne supportent qu'une seule adresse de destination. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémorisée. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela.
Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', séparée du ''Branch Target Buffer''. Leurs adresses de destination n'ont pas forcément besoin d'être mémorisées dans les lignes de cache.
La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K6 à K10 n'utilisaient pas cette méthode, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache. Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait que le branchement n'est pas pris si elle vaut 00, que c'est une instruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
6821i98aydkt2gmcm46srnxyi2aunb4
745806
745805
2025-07-02T19:19:40Z
Mewtow
31375
/* L'incorporation du Branch Target Buffer dans le cache d'instruction */
745806
wikitext
text/x-wiki
Les processeurs modernes disposent de plusieurs unités de calcul, de bancs de registres larges et de tout un tas d'optimisations permettent d’exécuter un grand nombre d'instructions par secondes. Les opérations de calcul, les accès mémoire : tout cela est très rapide. Mais rien de cela ne fonctionnerait si l'unité de chargement ne suivait pas le rythme. En soi, l'unité de chargement est simple : le ''program counter'', les circuits pour l'incrémenter et gérer les branchements, l'unité de prédiction de branchement, et de quoi communiquer avec le cache. On doit aussi ajouter le registre d'instruction. Difficile de trouver de quoi l'optimiser, à part rendre l'unité de prédiction plus efficace.
Pourtant, les processeurs incorporent diverses optimisations qui rendent le tout beaucoup plus rapide. La plupart de ces optimisations consistent à ajouter des files d'attente ou des mémoires caches dans le ''front-end'', que ce soit après l'étape de chargement ou de décodage. Les caches en question sont situés en aval du cache d'instruction, ce qui en fait des sortes de cache de niveau 0. Les optimisations incluent le préchargement d'instruction, l'usage de files d'attente pour découpler divers circuits et quelques autres. Voyons lesquelles dans ce chapitre.
==La file d'instruction et le cache de macro-opération==
L'unité de chargement contient de nombreux circuits fortement liés entre eux, et on peut découper le tout en plusieurs circuits. L'unité de calcul d'adresse émet les adresses des instructions à charger, qui sont consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions. L'unité de calcul d'adresse regroupe : l'unité de prédiction de branchement, le ''program counter'', le circuit pour incrémenter le ''program counter'', les MUX associés pour gérer les branchements.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Par exemple, l'unité de chargement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Pourtant, il est en théorie possible, et même utile, que certaines structures prennent de l'avance même si d'autres sont bloquées. Par exemple, si le pipeline est bloqué en aval de l'unité de chargement, l'unité de chargement peut en théorie précharger à l'avance des instructions. Ou encore, en cas de défaut de cache d'instruction, l'unité de calcul d'adresse peut précalculer les adresses destinées au cache et les mettre en attente. Pour cela, l'unité de chargement incorpore un paquet de mémoires FIFOs, que nous voir en détail dans ce qui suit.
===Les files d'instruction===
Les processeurs modernes intègrent une '''file d'instruction''', une mémoire FIFO, placée entre le cache d'instruction et le décodeur d'instruction. Les instructions chargées par l'étape de chargement soient accumulées dans la '''file d'instructions''' et sont décodées quand l'unité de décodage est prête.
La file d'attente permet de précharger des instructions dans la file d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. L'idée est que les instructions s'accumulent dans la file d'instruction si le processeur exécute les instructions moins vite qu'il ne les charge. C'est généralement signe qu'il effectue une instruction multicycle et/ou qu'il effectue un accès à la mémoire. À l'inverse, la file d'attente se vide quand le processeur éxecute les instructions plus vite qu'il n'en charge. C'est généralement signe qu'un défaut de cache d'instruction est en cours.
La présence d'une file d'attente fait que la première situation est compensée lors de la seconde. Les temps d'attentes liées aux instructions multicycles permettent de remplir la file d'attente, qui est ensuite vidée en cas de défaut de cache. Le processeur exécute en permanence des instructions, sans interruption. Alors que sans file d'attente, les défauts de cache entraineront des temps d'attente où le processeur s’exécuterait rien.
La seule limite de cette optimisation est l'influence des branchements. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu. Ce n'est ni plus ni moins ce que faisait la ''prefetch input queue'' des anciens processeurs Intel, dont nous avions parlé dans le chapitre sur l'unité de chargement et le séquenceur.
===Le cache de macro-opérations===
Le cache de macro-opérations est un cache présent en aval de l'unité de chargement, à côté de la file d’instruction. Il mémorise les dernières instructions envoyées à l'unité de décodage, à savoir non pas les instructions préchargées, mais celles qui sont en cours de décodage ou d’exécution, celles qui ont quitté la file d'instruction. Il sert dans le cas où ces instructions sont ré-éxecutées, ce qui est souvent le cas avec des boucles de petite taille.
A chaque cycle d'horloge, ce cache est consulté, de manière à vérifier si l'instruction voulue est dans ce cache ou non. Cela évite un accès au cache d'instruction. Son implémentation est simple : il s'agit d'un petit cache adressé par le ''program counter''. Si l'instruction a été chargée il y a peu, l'instruction machine est mémorisée dans une ligne de cache, le tag de cette ligne n'est autre que son adresse, le ''program counter'' associé. L'accès au cache de macro-opérations est de un seul cycle, pas plus.
[[File:Cache de macro-ops.png|centre|vignette|upright=2|Cache de macro-ops]]
L'intérêt n'est pas évident, mais disons que l'accès à ce cache gaspille moins d'énergie qu’accéder au cache d'instruction. C'est là l'intérêt principal, même s'il se peut qu'on puisse avoir un gain en performance. Le gain en question vient du fait que l'accès est plus rapide dans ce cache, ce qui n'est le cas que dans des conditions précise : si le cache d'instruction est pipeliné et a un temps d'accès de plusieurs cycles.
==La file de micro-opérations et le cache de micro-opérations==
[[File:File d'instruction.png|vignette|upright=1|File d'instruction]]
Sur les processeurs modernes, la sortie du décodeur est reliée à une mémoire FIFO semblable à la file d'instruction, mais placée juste après le décodeur. Elle mémorise les micro-opérations émises par le décodeur et les met en attente tant que le reste du pipeline n'est pas prêt. Nous l’appellerons la '''file de micro-opérations''', par simplicité. Le schéma ci-contre indique que la file de micro-opérations est située en sortie de l’unité de décodage, avant l'unité d'émission et avant l'unité de renommage de registres (que nous aborderons dans quelques chapitres).
La file de micro-opérations permet aux décodeurs de faire leur travail même si le reste du pipeline n'est pas prêt. Par exemple, imaginons que le processeur ne peut pas émettre de nouvelle instruction, soit car toutes les ALUs sont occupées, soit car il y a un accès mémoire qui bloque le pipeline, peu importe. Sans file de micro-opérations, tout ce qui précède l'unité d'émission devrait être totalement bloqué tant que l'instruction ne peut pas être émise. Mais avec une file de micro-opérations, le pipeline peut continuer à charger et décoder des instructions, et accumuler des instructions décodées dans la file de micro-opérations. En clair, la file de micro-opérations met en attente les instructions quand des bulles de pipeline sont émises.
Et à l'inverse, elle permet d'émettre des instructions quand les unités de décodage/chargement sont bloquées. Le cas classique est celui d'un défaut de cache dans le cache d'instruction. Des instructions ne peuvent plus être chargée et décodées durant quelques cycles. Sans file de micro-opérations, le processeur ne peut plus rien faire durant quelques cycles. Mais avec une file de micro-opérations, il peut en profiter pour émettre les instructions en attente dans la file de micro-opérations. En clair, si l'unité d'émission a mis en attente des instructions, le processeur se rattrape au prochain défaut de cache d'instruction.
Une autre situation où le décodeur bloque est le cas où certaines instructions mettent du temps à être décodées. C'est notamment le cas de certaines instructions complexes, dont le décodage prend facilement 2 à 3 cycles d'horloge, voire plus. Le pire est le décodage des instructions microcodées, qui peut demander plusieurs cycles. Or, le pipeline demande qu'on décode une instruction par cycle pour éviter de bloquer le pipeline. Mais ce temps de décodage peut être masqué si des micro-opérations sont en attente dans la file, elles sont exécutées pendant le décodage long.
La file de micro-opération est souvent complétée par plusieurs circuits, dont un circuit de micro-fusion, un cache de micro-opérations et le ''loop stream detector''. Voyons ces circuits dans ce qui suit.
[[File:File de micro-opérations et cache de micro-ops - Copie.png|centre|vignette|upright=2.5|File de micro-opérations et cache de micro-ops - Copie]]
===Le ''Loop Stream Detector''===
Les boucles sont une opportunité d'optimisation très intéressante sur les CPU avec une file de micro-opérations. L'idée est que lors d'une boucle, des instructions sont chargées, décodées et exécutées plusieurs fois de suite. Mais à, chaque répétition d'une instruction, le chargement et le décodage donnent toujours le même résultat, seule l'exécution n'est pas la même (les registres renommés sont aussi différents, mais passons). L'idée est simplement de mémoriser les N dernières instructions décodées et de les ré-exécuter si besoin. Ainsi, on évite de charger/décoder une même instruction machine plusieurs fois, mais de réutiliser les micro-opérations déjà décodées.
L'implémentation la plus simple conserve les N dernières instructions décodées dans la file d'instruction, qui se comporte alors comme une sorte de pseudo-cache FIFO. Un circuit annexe, appelé le ''Loop Stream Detector'' (LSD), détecte lesboucles dans la file de micro-opérations et optimise leur exécution. Avec un LSD, la file d'instruction ne supprime pas les micro-opérations une fois qu'elles sont émises. Elle mémorise là où se trouve la dernière micro-opération émise, mais conserve celles qui ont déjà été émises. Si une boucle adéquate est détectée par le ''Loop Stream Detector'', les micro-opérations de la boucle sont lues dans la file de micro-opération et sont injectées directement dans la suite du pipeline. De plus, les unités de chargement et de décodage sont désactivées pendant l’exécution de la boucle, ce qui réduit la consommation d'énergie du CPU.
L'optimisation accélère les petites boucles, à condition qu'elles s'exécutent de la même manière à chaque exécution. De telles boucles exécutent une suite de N instructions, qui reste identique à chaque itération de la boucle. Le cas le plus simple est celui d'une boucle dans laquelle il n'y a pas de branchements. Pour les boucles normales, le processeur reprend une exécution normale quand on quitte la boucle ou quand son exécution change, par exemple quand un if...else, un return ou tout autre changement de flot de contrôle a lieu. Vu que toutes ces situations impliquent un branchement qui n'a pas été pris comme avant, le processeur n'utilise plus le ''Loop Stream Detector'' en cas de mauvaise prédiction de branchement.
L'optimisation vise surtout à désactiver les décodeurs et l'unité de chargement lors de l'exécution d'une boucle. La désactivation peut être du ''clock gating'', voire du ''power gating'', être partielle ou totale. Dans le pire des cas, les unités de chargement peuvent continuer à charger des instructions en avance dans une file d'instruction, mais les décodeurs peuvent être désactivés. Dans le meilleur des cas, la totalité de ce qui précède la file de micro-opération est désactivé tant que la boucle s’exécute normalement. Y compris le cache de micro-opération.
[[File:Loop Stream Detector.png|centre|vignette|upright=2|Loop Stream Detector]]
Les CPU Intel modernes disposent d'un ''loop stream detector'', les CPU AMD en avaient sur les microarchitectures Zen 4 mais il a disparu sur la microarchitecture Zen 5. Quelques CPU ARM avaient aussi un ''loop stream detector'', notamment le Cortex A15. Évidemment, la taille des boucles optimisées ainsi est limitée par la taille de la file de micro-opération, ce qui fait que l'optimisation ne fonctionne que pour des boucles de petite taille. De plus, toute la file de micro-opération n'est pas gérée par le ''loop stream detector''. Par exemple, les processeurs avec une file de micro-opération de 64 micro-opération peuvent gérer des boucles de maximum 32 à 40 micro-opérations. Pour donner quelques chiffres, les processeurs ARM Cortex A15 géraient des boucles de maximum 32 micro-opérations.
Mais les contraintes principales portent sur la détection des boucles. Le ''Loop Stream Detector'' ne peut pas détecter toutes les boucles qui existent, et certaines boucles ne sont pas détectées. Par exemple, le ''Loop Stream Detector' ne peut pas détecter les boucles si un appel de fonction a lieu dans la boucle. Il y a aussi des contraintes quant au nombre de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Il faut noter que le ''loop stream detector'' a été désactivé par des mises à jour de microcode sur quelques architectures, comme sur la microarchitecture Zen 4 d'AMD ou les CPU de microarchitecture Skylake et Kaby Lake d'Intel. Pour la microarchitecture Skylake , les raisons officielles pour cette désactivation sont un bug lié à l'interaction avec l'''hyperthreading''. Il est vraisemblable que des bugs ou des problèmes de sécurité aient amené à la désactivation sur les autres architectures.
===Le cache de micro-opérations===
Le '''cache de micro-opérations''' a le même but que le ''Loop Stream Detector'', à savoir optimiser l'exécution des boucles. La différence avec le ''Loop Stream Detector'' est qu'il y a un cache séparé de la file de micro-opérations, qui mémorise des micro-opérations décodées, dans le cas où elles soient réutilisées par la suite. La première itération d'une boucle décode les instructions en micro-opérations, qui sont accumulées dans le cache de micro-opérations. Les itérations suivantes de la boucle vont chercher les micro-opérations adéquates dans le cache de micro-opération : on n'a pas à décoder l'instruction une nouvelle fois.
Intuitivement, vous vous dites que son implémentation la plus simple mémorise les N dernières micro-opérations exécutées par le processeur, ce qui en fait un cache FIFO. Mais la réalité est que c'est déjà ce qui est fait par le couple LSD + file de micro-opération. Le cache de micro-opérations a une politique de remplacement des lignes de cache plus complexe que le FIFO, typiquement une politique LRU ou LFU approximée. De plus, le cache de micro-opération est séparé de la file de micro-opération. Et il est alimenté non pas par l'unité de décodage, mais par la file de micro-opérations. Ce sont les micro-opérations qui quittent la file de micro-opérations qui sont insérées dans le cache, pas celles qui quittent directement le décodeur.
Les avantages sont les mêmes qu'avec un ''Loop Stream Detector'' : une consommation énergétique réduite, des performances légèrement améliorées. Le décodeur et l'unité de chargement sont inutiles en cas de succès dans le cache de micro-opération, ce qui fait qu'ils sont désactivés, éteints, ou du moins subissent un ''clock-gating'' temporaire. Ils ne consomment pas d'énergie, seul le cache de micro-opération utilise de l'électricité. L'avantage en termes de performance est plus faible, assez variable suivant la situation, mais aussi bien le cache de micro-opérations que le LSD ne font pas de mal.
La différence avec le cache de micro-opération est que la boucle doit s’exécuter à l'identique avec un ''Loop Stream Detector'', pas avec un cache de micro-opérations. Prenons l'exemple d'une boucle contenant quelques instructions suivies par un IF...ELSE. Il arrive qu'une itération de la boucle exécute le IF, alors que d'autres exécutent le ELSE. Dans ce cas, le ''Loop Stream Detector'' ne sera pas activé, car la boucle ne s’exécute pas pareil d'une itération à l'autre. Par contre, avec un cache de macro/micro-opération, on pourra lire les instructions précédant le IF...ELSE dedans. Le cache de micro-opération est donc plus efficace que le ''Loop Stream Detector'', mais pour un cout en transistor plus élevé.
Le cache de micro-opérations et le ''Loop Stream Detector'' font la même chose, mais certains processeurs implémentaient les deux. L'avantage est que le cache de micro-opération peut être désactivé si jamais le LSD détecte une boucle dans la file d'instruction, ce qui réduit encore plus la consommation énergétique. En pratique, l'impact sur la consommation énergétique est très difficile à mesurer, mais il rajoute de la complexité pour la conception du processeur.
[[File:File de micro-opérations et cache de micro-ops.png|centre|vignette|upright=2|File de micro-opérations et cache de micro-ops]]
Le cache de micro-opération associe, pour chaque instruction machine, une ou plusieurs micro-opérations. Avec l'implémentation la plus simple, une ligne de cache est associée à une instruction machine. Par exemple, sur les processeurs Intel de microarchitecture Skylake, chaque ligne de cache était associée à une instruction machine et pouvait contenir de 1 à 6 micro-opérations. La suite de micro-opérations correspondant à une instruction devait tenir toute entière dans une ligne de cache, ce qui fait que les instructions décodées en plus de 6 micro-opérations ne pouvaient pas rentrer dans ce cache.
L'accès au cache de micro-opération se fait lors de l'étape de chargement. Le cache de micro-opérations est adressé en envoyant le ''program counter'' sur son entrée d'adresse, en parallèle du cache d'instruction. Le cache de micro-opération est une voie de chargement parallèle au ''front-end'' proprement dit. En clair, il y a une voie qui regroupe cache d'instruction, file d'instruction et décodeur, et une seconde voie qui se résume au cache de micro-opération. Les deux voies sont accédées en parallèle. En cas de succès dans le cache de micro-opération, les micro-opérations adéquates sont lues directement depuis le cache de micro-opération.
Il existe deux méthodes différentes pour encoder les micro-opérations dans le cache de micro-opérations. La première est la plus intuitive : on mémorise les micro-opérations dans la ligne de cache, directement. Elle est utilisée sur les processeurs AMD, et sans doute sur les processeurs Intel récents. Mais les anciens processeurs Intel, comme ceux des architectures Sandy Bridge et Netburst, utilisent une autre méthode. Une ligne de cache mémorise non pas les micro-opération directement, mais un pointeur vers le ''control store'', qui indique à quelle adresse dans le micro-code se situe la micro-opération. La micro-opération est donc lue depuis le micro-code lors de l'émission.
Il faut noter que pour des raisons de performance, le cache de micro-opérations est virtuellement tagué, ce qui fait qu'il est invalidé en cas de changement de programme. Sur l'architecture Sandy Bridge, il est carrément inclus dans le cache L1, les deux sont des caches inclusifs l'un avec l'autre. Les premières implémentations étaient très limitées. Les micro-opérations devaient être séquentielles dans le code, le cache était consulté seulement après un branchement et non à chaque émission d'instruction, pour limiter la consommation d'énergie an détriment des performances. Ces limitations ne sont pas présentes sur les architectures récentes.
Aussi bien le cache de macro-opérations que le cache de micro-opérations optimisent l'exécution des boucles, mais ils ne sont pas au même endroit dans le pipeline : avant et après l'unité de décodage. Et le premier mémorise des instructions machines, l'autre des micro-opérations décodées. Les avantages et inconvénients sont totalement différents. Niveau capacité des deux caches, l'encodage des instructions machines est plus compact que la ou les micro-instructions équivalente, ce qui est un avantage pour le cache de macro-opérations à capacité équivalente. Par contre, le cache de micro-opérations permet de désactiver les décodeurs en cas de succès de cache, vu que les instructions ne doivent plus être décodées et renommées. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable, ce qui rend leur décodage complexe et donc lent. Globalement, plus le décodage est complexe et/ou long, plus le cache de micro-opérations fait des merveilles.
==Le préchargement d'instructions et la ''Fetch Target Queue''==
Les processeurs modernes incorporent une optimisation assez intéressante : ils découplent l'unité de prédiction de branchement et le ''program counter'' de l'accès au cache d'instruction. Pour cela, ils incorporent une mémoire FIFO entre l'unité de prédiction de branchement et le cache d'instruction. Les premiers articles scientifiques, qui ont proposé cette solution, l'ont appelée la '''''Fetch Target Queue''''', abréviée FTQ. Elle accumule les adresses à lire/écrire dans le cache d'instruction, peu importe que ces adresses viennent du ''program counter'' ou de l'unité de prédiction de branchement.
[[File:Fetch target queue.png|centre|vignette|upright=2.5|Fetch target queue]]
Elle se remplit quand le cache d'instruction est bloqué, soit à cause d'un défaut de cache, soit à cause d'un pipeline bloqué en amont de l'unité de chargement. Par exemple, si le cache d'instruction est bloqué par un défaut de cache, l'unité de prédiction de branchement peut accumuler des prédictions à l'avance dans la FTQ, qui sont ensuite consommées par le cache d'instruction une fois qu'il est redevenu disponible. De même, si l'unité de prédiction de branchement est bloquée par un évènement quelconque, le cache d'instruction peut consommer les prédictions faites à l'avance.
Une utilisation assez originale de la FTQ s'est vu sur les processeurs AMD d'architectures bulldozer. Sur cette architecture, les cœurs étaient regroupés par paquets de deux, et les deux cœurs partageaient certains circuits. Notamment, l'unité de prédiction de branchement était partagée entre les deux cœurs ! Pourtant, chaque cœur disposait de sa propre FTQ !
Un avantage de la FTQ tient dans le fait que les caches d'instructions sont pipelinés, sur le même modèle que les processeurs. On peut leur envoyer une demande de lecture/écriture par cycle, alors que chaque lecture/écriture prendra plusieurs cycles à s'effectuer. L'accès au cache d'instruction a donc une certaine latence, qui est partiellement masquée par la FTQ au point où elle ne s'exprime qu'en cas de défaut de cache assez important. Par exemple, si l'accès au cache d'instruction prend 4 cycles, une FTQ qui met en attente 4 adresses camouflera le temps d'accès au cache, tant qu'il n'y a pas de mauvaise prédiction de branchement. La FTQ est aussi très utile avec les unités de branchement modernes, qui peuvent mettre plusieurs cycles pour fournir une prédiction. Prendre de l'avance avec une FTQ amorti partiellement le temps de calcul des prédictions.
: Si le cache d'instruction est multiport et accepte plusieurs accès simultanés, il peut consommer plusieurs entrées dans la FTQ à la fois.
Mais l'avantage principal de la FTQ est qu'elle permet l'implémentation d'une optimisation très importante. Il y a quelques chapitres, nous avions parlé des techniques de '''préchargement d'instruction''', qui permettent de charger à l'avance des instructions dans le cache d'instruction. Nous avions volontairement laissé de côté le préchargement des instructions, pour tout un tas de raisons. Et la raison est justement que la prédiction de branchement et le préchargement des instructions sont fortement liés sur les processeurs modernes. Il est maintenant possible d'aborder le préchargement pour les instructions, d’où cette section.
Notons que par préchargement des instructions, on peut parler de deux formes de préchargement, fortement différentes. La première correspond au préchargement normal, à savoir le préchargement des instructions dans le cache d'instruction L1, à partir du cache L2. Il s'agit donc d'un préchargement dans le cache d'instruction. Mais il existe aussi une autre forme de préchargement, qui consiste à précharger à l'avance des instructions dans la file d'instruction et qui a été abordée dans la section sur la ''prefetch input queue''. Les deux formes de préchargement n'ont pas lieu au même endroit dans la hiérarchie mémoire : l'une précharge du cache L2 vers le L1i, l'autre du cache L1i vers la file d'instruction (ou dans le cache de macro-opération). Mais les algorithmes utilisés pour sont sensiblement les mêmes. Aussi, nous allons les voir en même temps. Pour faire la distinction, nous parlerons de préchargement L2-L1i pour la première, de préchargement interne pour l'autre.
===Les algorithmes de préchargement d'instructions===
Les techniques basiques de préchargement consistent à charger des instructions qui suivent la dernière ligne de cache accédée. Quand on charge des instructions dans le cache d’instruction, les instructions qui suivent sont chargées automatiquement, ligne de cache par ligne de cache. il s'agit due préchargement séquentiel, la technique la plus simple de préchargement, qui profite de la localité spatiale. Elle est utilisée pour précharger des instructions du cache L2 vers le cache L1i, mais aussi pour le préchargement interne dans la file d'instructions.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Mais un ''prefetcher'' purement séquentiel gère mal les branchements. Si un branchement est pris, les instructions de destination ne sont pas chargées, si elles ne sont pas dans la ligne de cache suivante. Pour le préchargement L2-L1i, cela ne pose pas de problèmes majeurs, au-delà de la pollution du cache L1i par des instructions inutiles. Mais pour le préchargement interne, c'est autre chose. Les instructions préchargées par erreurs doivent être supprimées pour éviter qu'elles soient décodées et exécutées, ce qui fait que la file d’instruction doit être invalidée.
Il existe des techniques de préchargement plus élaborées qui marchent mieux en présence de branchements. Elles utilisent toutes une collaboration de l'unité de prédiction de branchement. Elles accèdent au ''Branch Target Buffer'', pour détecter les branchements, leur destination, etc. Le tout peut se coupler à la technique du prédécodage. Avec cette dernière, le prédécodage décode en partie les instructions lors de leur chargement dans le cache, et détecte les branchements et leur adresse de destination à ce moment-là. Ces informations sont alors mémorisées dans une table à part, ou dans le BTB. Mais la plupart des designs utilisent le BTB, par souci de simplicité. Il existe globalement deux à trois techniques principales, que nous allons voir dans ce qui suit.
La première technique prédit si le branchement est pris ou non, et agit différemment si le branchement est pris ou non. Si le branchement est pris, elle précharge les instructions à partir de l'adresse de destination des branchements pris. Sinon, elle précharge les instructions suivantes avec préchargement séquentiel. Il s'agit du '''''target line prefetching'''''
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Une autre technique ne prédit pas les branchements et précharge à la fois les instructions suivantes avec le ''next-line prefetching'', et la ligne de cache de destination du branchement avec le ''target line prefetching''. Comme ça, peu importe que le branchement soit pris ou non, les instructions adéquates seront préchargées quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
Le ''target line prefetching'' est plus complexe à implémenter, car il demande de prédire les branchements. Mais elle a l'avantage de ne pas précharger inutilement deux lignes de cache par branchement, seulement une seule. Par contre, le préchargement est inutile en cas de mauvaise prédiction de branchement : non seulement on a préchargé une ligne de cache inutilement, mais en plus, la ligne de cache adéquate n'a pas été chargée. On n'a pas ce problème avec le préchargement du mauvais chemin, qui garantit que la ligne de cache adéquate est toujours préchargée.
===L'implémentation du préchargement interne, dans la file d'instruction===
Le préchargement dans la file d'instruction est généralement de type séquentiel, mais certains processeurs font autrement. Déjà, il faut remarquer que le ''target line prefetching'' correspond en réalité à la prédiction de branchement classique. L'adresse de destination est prédite, et on charge les instructions adéquates dans la file d'instruction. La prédiction de branchement, associée à une file d'instruction, est donc une forme de préchargement. Il fallait y penser. Enfin, des processeurs assez rares utilisaient le préchargement du mauvais chemin.
Le préchargement du mauvais chemin demande d'utiliser deux files d'instructions séparées. L'une dans laquelle on précharge de manière séquentielle, l'autre dans laquelle on utilise la prédiction de branchement pour faire du ''target line prefetching''. Une fois que l'on sait si la prédiction de branchement était correcte, on est certain qu'une des deux files contiendra les instructions valides. Le contenu de la file adéquate est conservé, alors que l'autre est intégralement invalidée. Le choix de la bonne file se fait avec un multiplexeur. C'est approximativement la technique qui était implémentée sur le processeur de mainframe IBM 370/165, par exemple, et sur quelques modèles IBM similaires.
Le problème est que cette méthode demande de charger deux instructions à chaque cycle. Cela demande donc d'utiliser un cache d'instruction multiport, avec un port par file d'instruction. Le cout en circuit d'un cache double port n'est pas négligeable. Et le gain en performance est assez faible. Le préchargement dans la file d’instruction permet d'économiser quelques cycles lors de l'accès au cache d'instruction, guère plus. Le gain est maximal lorsque les instructions préchargées ont généré un défaut de cache, qui a rapatrié les instructions adéquates pendant que le processeur exécutait les mauvaises instructions, avant que la mauvaise prédiction de branchement soit détectée. Dans ce cas, le défaut de cache a eu lieu pendant la mauvaise prédiction et sa réparation, et non après.
====La gestion des branchements successifs====
Un autre défaut de cette méthode est la présence de branchements successifs. Par exemple, si jamais on rencontre un branchement, le flux d'instructions se scinde en deux : un où le branchement est pris, un autre où il ne l'est pas. Chacun de ces flux peut lui-même contenir un branchement, et se scinder lui aussi. Et ainsi de suite. Et le processeur doit gérer cette situation en termes de préchargement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
Plusieurs solutions existent. La méthode la plus simple stoppe le chargement du flux en attendant que le premier branchement soit terminé. Cette solution est intuitive, mais est celle où on a les gains en performance les plus faibles. Elle est couramment implémentée d'une manière assez particulière, qui ne correspond pas tout à fait à un stop du chargement, mais qui utilise les lignes de cache. L'unité de préchargement est conçue pour copier des lignes de cache entières dans la file d'instruction. Le processeur (pré-)charge deux lignes de cache : celle du bon chemin, celle du mauvais chemin. Il les précharge dans deux files d'instructions, qui contiennent généralement une ligne de cache grand maximum. Le temps que l'on ait chargé les deux files d'instruction, le résultat du branchement est connu et on sait laquelle est la bonne.
L'autre possibilité est d'utiliser la prédiction de branchement pour ce flux, afin de poursuivre le chargement de manière spéculative. Elle donne de bonnes performances, mais demande des unités de prédiction de branchement spéciales, dans le cas où les deux flux tombent sur un branchement en même temps. Cette technique est indirectement liée au cache de traces que nous verrons dans le chapitre sur les processeurs superscalaires. Nous n'en parlons pas ici, car ce genre de techniques est plus liée aux processeurs superscalaires qu'un processeur avec un pipeline normal.
Une autre possibilité consiste à scinder ce flux en deux et charger les deux sous-flux. Cette dernière est impraticable car elle demande des caches avec un grand nombre de ports et la présence de plusieurs files d'instructions, qui sont utilisées assez rarement.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
====Les processeurs à exécution de chemins multiples====
L'idée précédente peut en théorie être améliorée, afin de non seulement charger les instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris), mais aussi de les exécuter : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin.
Quelques papiers de recherche ont étudié l'idée, mais ses défauts font qu'elle n'a jamais été utilisée dans un processeur en dehors de prototypes destinés à la recherche. Le gros problème de l'exécution stricte est qu'on est limité par le nombre d'unités de calculs, de registres, etc. Autant ce serait une technique idéale sur des processeurs avec un nombre illimité de registres ou d'unités de calcul, autant ce n'est pas le cas dans le monde réel. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles et doit soit stopper, soit recourir à la prédiction de branchement. Il y a le même problème avec le préchargement interne simple, quand on utilise le préchargement du mauvais chemin, comme vu juste au-dessus.
===L'implémentation matérielle du préchargement de cache L2-L1i===
Pour comprendre comment s'effectue le préchargement L2-L1i, il faut regarder comment l'unité de chargement communique avec les caches. L'unité de prédiction de branchement est généralement regroupée avec le ''program counter'' et les circuits associés (les incrémenteurs/MUX associés), pour former l'unité de chargement proprement dite. L'unité de chargement émet des adresses consommées par le cache d'instruction, qui lui-même envoie les instructions lues dans le registre d'instruction ou la file d'instructions.
Le couplage de ces structures fait qu'au moindre défaut de cache d'instruction, l'ensemble stoppe. Et notamment, l'unité de prédiction de branchement stoppe en cas de défaut de cache. Même chose si jamais une instruction multicycle s’exécute dans le pipeline et bloque toutes les étapes précédentes. Les pertes de performance ne sont pas très importantes, mais elles existent. Et le préchargement se manifeste dans ces situations.
Le préchargement d'instructions consiste à découpler ces structures de manière à ce qu'elles fonctionnent plus ou moins indépendamment. Le but est qu'en plus des accès normaux au cache d'instruction, l'unité de chargement envoie des informations au cache L2 ou L1i en avance, pour effectuer le préchargement. L'unité de chargement doit alors prendre de l'avance sur le cache, pour effectuer les accès au cache L2 en avance, tout en maintenant l'état normal pour effectuer les accès normaux. C'est donc plus ou moins l'unité de chargement qui s'occupe du préchargement, ou du moins les deux sont très liées.
====L'anticipation du ''program counter''====
Avec la solution la plus simple, on a une unité de chargement qui s'occupe des accès au cache d'instruction, et une unité de préchargement qui prend de l'avance sur l'unité de chargement, et communique avec le cache L2. La technique la plus basique se base sur un ''Lookahead program counter'', un second ''program counter'' qui ne fonctionne que lors d'un défaut de cache d'instruction. Il est initialisé avec le ''program counter'' lors d'un défaut de cache, puis il est incrémenté à chaque cycle et les branchements sont prédits, ce qui fait qu'il est mis à jour comme si l’exécution du programme se poursuivait, alors que le reste du processeur est mis en attente.
La technique initiale utilisait ce second ''program counter'' pour accéder à une table de prédiction, qui associe à chaque valeur du ''program counter'', l'adresse des données chargées par l'instruction associée. Les adresses fournies à chaque cycle par cette table sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. La technique permettait donc de précharger des données en cas de défaut de cache, mais pas d'instructions. Il ne s'agissait pas d'une technique de préchargement des instructions, mais de préchargement de données.
La technique a ensuite été adaptée pour le chargement des instructions par Chen, Lee et Mudge. Leur idée utilisait deux unités de prédiction de branchements : une couplée à l'unité de chargement, l'autre pour le préchargement. La première utilisait le ''program counter'' normal, l'autre se déclenchait en cas de défaut de cache et utilisait un ''lookahead program counter''. Les adresses générées par le ''lookahead program counter'' étaient envoyée au cache d'instruction, sur un port de lecture séparé. La ligne de cache lue était alors prédécodée pour détecter les branchements, qui étaient prédits, et rebelote. Il est possible d'adapter la méthode pour que les adresses soient accumulées dans une mémoire FIFO, et étaient consommée par le cache d'instruction L2 pour le préchargement si la ligne de cache associée n'était pas dans le cache d’instruction.
Les techniques modernes n'utilisent plus de seconde unité de prédiction de branchement, mais conservent un ''lookahead program counter''. Par contre, le BTB dispose de plusieurs ports : un pour la prédiction de branchement normale, l'autre pour le préchargement. L'unité de préchargement et l'unité de chargement accèdent toutes deux au BTB quand elles ont besoin de faire leurs prédictions, en parallèle. Typiquement, le BTB est accédé à chaque cycle pour la prédiction de branchement, à un rythme plus faible pour le préchargement.
====Le ''Fetch Directed Instruction Prefetching''====
Les processeurs modernes semblent utiliser un algorithme connu sous le nom de '''''Fetch Directed Instruction Prefetching'''''. Il utilise les adresses contenues dans la FTQ pour précharger les instructions adéquates du cache L2 vers le cache L1 d'instruction (L1i). L'unité de préchargement est placée en aval de la FTQ, elle lit son contenu, détecte quelles adresses correspondent à des lignes de cache à précharger, et envoie celles-ci au cache L2. Le préchargement du L2 vers le L1i a lieu quand le cache L2 est inutilisé, ou du moins quand il peut accepter une nouvelle lecture (dans le cas d'un cache multiport et/ou pipeliné).
[[File:Fetch directed instruction prefetching.png|centre|vignette|upright=2.5|Fetch directed instruction prefetching]]
On peut améliorer légèrement le design précédent sur plusieurs points. Pour éviter de polluer le cache L1 avec des lignes de caches préchargées à tort, il est possible d'ajouter un équivalent des ''stream buffer'' vus dans le chapitre sur le préchargement. Il s'agit d'une autre mémoire FIFO qui mémorise les lignes de cache préchargées. Les lignes de cache préchargées ne sont pas placées dans le cache L1i, mais dans cette file d'attente. Lors d'un accès au L1i, la file d'attente est consultée en parallèle. Si l'instruction voulue est dans la file d'attente, elle est lue depuis la file, et la ligne de cache associée est copiée dans le cache L1i. Mais c'est là une possibilité facultative.
Un autre point est que l'unité de préchargement doit attendre que le cache L2 puisse accepter une nouvelle lecture pour lancer le préchargement d'une autre ligne de cache. Pour corriger cela, on ajoute une file d'attente entre le cache L2 et l'unité de préchargement, qui est évidemment une mémoire FIFO. Son utilité dépend des temps de lectures du cache L2, ainsi que de la taille de la FTQ. Elle n'est pas toujours nécessaire, certains processeurs ont un cache L2 assez lent pour qu'on ne puisse précharger qu'une seule ligne de cache avant que la FTQ soit complétement vide.
Ces deux optimisations sont facultatives, mais elles étaient présentes dans l'article originel qui a proposé la technique.
L'unité de préchargement doit détecter quelles sont les adresses de la FTQ qui ne sont pas déjà chargées dans le L1i. En effet, il est inutile de précharger une ligne de cache si celle-ci est déjà dans le cache L1i. L'unité de préchargement doit donc filtrer au mieux les adresses de la FTQ en deux classes : celles qui correspondent à une ligne de cache déjà dans le L1i, celles qui doivent être préchargées.
Pour cela, l'unité de préchargement utilise la technique dit du '''''Cache Probe Filtering'''''. L'idée part du principe que le cache d'instruction L1 est multiport. Les ports du cache d'instruction ne sont pas toujours utilisés en même temps et il arrive qu'il y ait un port de lecture de libre. Le CPF utilise alors ce port inutilisé pour vérifier si la prochaine ligne de cache à précharger est dans le cache ou non. Si c'est le cas, on aura un succès de cache : la ligne de cache est oubliée, elle ne sera pas préchargée. Si ce n'est pas le cas on aura un défaut de cache : la ligne sera préchargée.
Notez que l'on a pas besoin de lire la ligne en question, juste de vérifier les tags du cache. Dans ce cas, on peut ajouter des signaux de commande spécifiques pour le CPF, qui font une demi-lecture, qui ne vérifie que les tags, mais ne lit pas la donnée. On peut par exemple ajouter un port spécifique pour le CPF, purement en lecture et qui ne permet que de vérifier les tags. Ce port en plus a un cout en circuits plus faible qu'un port de lecture normal, mais ce n'est pas gratuit du tout.
==Les unités de prédiction couplées au cache d'instruction==
Dans la section précédente, nous venons de voir ce qu'il se passe quand on découple l'unité de prédiction de branchement du cache, en insérant une mémoire FIFO entre les deux. Mais d'autres processeurs font l'exact inverse : ils incorporent une partie de la prédiction de branchement dans le cache L1 d'instruction. Les premiers processeurs AMD faisaient ainsi, en stockant des informations de prédiction de branchement dans le cache d'instruction. Une ligne de cache contenait ainsi des informations de prédiction de branchement dans ses bits de contrôle. Les informations en question peuvent être des adresses de destination, ou simplement de quoi déterminer si le branchement est pris ou non.
===Les sélecteurs de branchement intégrés au cache L1===
La technique que nous allons est une amélioration de la technique du prédécodage, qui décode partiellement les instructions lors de leur entrée dans le cache L1. Une ligne de cache contient potentiellement plusieurs branchements, dont la position est identifiée par le prédécodage. Pour chaque octet, la ligne de cache associe un bit de contrôle qui indique si un branchement démarre à cet octet, si c'est le premier octet d'un branchement. Le prédécodage peut identifier entre un et plusieurs branchement par ligne de cache, il y a une limite. Le prédécodage n'identifie typiquement que les 3 à 5 premiers branchements, les suivants sont ignorés, faute de place dans les bits de contrôle.
Prenons par exemple une ligne de cache de 8 octets, dans laquelle on a 2 branchements de 2 octets chacun.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Branch 1 || bgcolor="#FFFF00" | Branch 1 || Instruction || bgcolor="#FFFF00" | Branch 2 || bgcolor="#FFFF00" | Branch 2 || Instruction || Instruction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 0 || 1 || 0 || 0 || 1 || 0 || 0 || 0
|}
Il est possible d'améliorer le tout en précisant quel est le type du branchement. Par exemple, on peut distinguer les branchements inconditionnel et conditionnels, ou encore les instruction de retour de fonction. L'intérêt n'est pas évident, mais c'est lié au fait que les branchements inconditionnels sont toujours pris, et que les retour de fonction ont une adresse de destination qui est prédite par une unité de branchement séparée, le ''return adress predictor'', pas par un BTB. Deux bits suffisent pour indiquer : si c'est un branchement conditionnel, inconditionnel, un retour de fonction, ou une instruction qui n'est pas un branchement.
{|class="wikitable" style="text-align:center;"
|-
! colspan="16 | Ligne de cache, en octets
|-
| Instruction || bgcolor="#FFFF00" | Saut inconditionnel || bgcolor="#FFFF00" | Saut inconditionnel || Instruction || bgcolor="#A00000" | Branch cond || bgcolor="#A00000" | Branch cond || Instruction || bgcolor="#F0F000" | Retour de fonction
|-
! colspan="16 | Bits d'identification des branchements.
|-
| 00 || 01 || 00 || 00 || 10 || 00 || 00 || 11
|}
L'idée est alors d'ajouter, pour chaque branchement détecté, un '''sélecteur de branchement''' qui indique si le branchement est pris ou non. En clair, des informations de prédiction de branchement sont ajoutés à chaque octet de position. Intuitivement, on se dit qu'il y a seulement un bit par branchement, qui indique si le branchement est pris ou non.
Les prédictions peuvent venir soit de l'unité de prédiction de branchement, soit provenir du prédécodage. Le prédécodage peut faire de la prédiction statique. Elle peut notamment détecter les branchements inconditionnels et les marquer comme pris. Elle peut aussi détecter les branchements conditionnels et le marquer comme non-pris par défaut. L'unité de prédiction de branchement met à jour les sélecteurs de branchements si besoin, pour les branchements conditionnels.
===L'incorporation du ''Branch Target Buffer'' dans le cache d'instruction===
Une première optimisation permet de se passer de ''Branch Target Buffer''. Pour rappel, celui-ci est un cache qui mémorise, pour chaque branchement, quelle est son adresse de destination. Il peut contenir d'autres informations de prédiction, mais laissons-les de côté pour le moment.
L'idée est de déplacer les adresse de destination des branchements dans le cache d'instruction, dans les lignes de cache. 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. En général, les processeurs ne supportent qu'une seule adresse de destination. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémorisée. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela.
Il faut cependant remarquer qu'à ce petit jeu, les instructions de retour de fonction sont à part. Leur adresse de destination est souvent donnée par une unité de branchement séparée, le ''return adress predictor'', séparée du ''Branch Target Buffer''. Leurs adresses de destination n'ont pas forcément besoin d'être mémorisées dans les lignes de cache.
La technique décrite ici est simple à comprendre. Cependant, les processeurs AMD anciens, d'architecture K6 à K10 n'utilisaient pas cette méthode, mais une variante plus complexe, capable de prédire jusqu'à deux adresses de destination par branchement. A partir de l'architecture K6, le prédécodage déterminait la position des branchements dans les lignes de cache, dans une limite de 4 branchements par ligne de cache. Pour chaque branchement, la ligne de cache mémorisait un sélecteur de branchement, codé sur 2 bits. La valeur des bits indiquait que le branchement n'est pas pris si elle vaut 00, que c'est une instruction de retour de fonction si elle vaut 01, qu'il faut brancher à l'adresse de destination X si elle vaut 10, qu'il faut brancher à l'adresse de destination X si elle vaut 11. Les adresses de destination sont quand à elles mémorisées dans un cache séparé, appelé le ''Branch Target Cache''. Le mécanisme pour adresser ce cache à partir du cache d'instruction n'est pas très détaillé dans la documentation d'AMD.
===Les avantages et inconvénients===
L'avantage de faire ainsi est que la prédiction de branchement est plus rapide. Lire une instruction depuis le cache renvoie non seulement l'instruction lue, mais aussi des informations de prédiction de branchement. L'unité de prédiction de branchement peut alors utiliser ces informations au cycle suivant pour savoir quelle est l'instruction suivante à charger.
Un défaut de cette approche est que si le branchement à prédire 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 les adresses de destination et la direction d'un branchement, tant que l'entrée associée est dans le BTB. Et l'entrée peut être conservée, même si l'instruction en question a quitté le cache L1 et qu'elle est dans le L2, le L3 ou même en mémoire RAM. Les prédictions peuvent même servir à précharger les instructions utiles.
Sur l'Itanium et l'AMD Opteron, une optimisation assez intéressante permet de conserver les prédictions de branchement lorsque l'un branchement est évincé du cache L1 et se retrouve dans le cache L2. En théorie, les informations de prédiction, présentes dans la ligne de cache, sont perdues lorsque le branchement est évincé. Mais ces processeurs conservent ces prédictions dans un cache séparé, appelé le '''''L2 Branch Cache'''''.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La prédiction de branchement
| prevText=La prédiction de branchement
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
{{AutoCat}}
eho8xes44w1u3zpy6qjjke6pdzg1myr
Mathc matrices/a209
0
80098
745853
725305
2025-07-03T10:24:55Z
Xhungab
23827
745853
wikitext
text/x-wiki
[[Catégorie:Mathc matrices (livre)]]
[[Mathc matrices/a259| '''Gass-Jordan''']]
{{Partie{{{type|}}}|Analyse de réseaux}}
{| class="wikitable"
|+ Premier exemple : ''Résoudre avec [[Mathc matrices/06g| QR]]
|-
| * [[Mathc matrices/a210| Présentation]] || [[File:Network Analysis Using Linear Systems ex 01a.png|thumb|]]|| * [[Mathc matrices/a211| Résolution]] || [[File:Network Analysis Using Linear Systems ex 01b.png|thumb|]]
|}
{| class="wikitable"
|+ Deuxième exemple : ''Résoudre avec [[Mathc matrices/06h| QR]]
|-
| * [[Mathc matrices/a212| Présentation]] || [[File:Network Analysis Using Linear Systems ex 02a.png|thumb|]]|| * [[Mathc matrices/a213| Résolution]] || [[File:Network Analysis Using Linear Systems ex 02b.png|thumb|]]
|}
{| class="wikitable"
|+ Troisième exemple : ''Résoudre avec [[Mathc matrices/06i| QR]]
|-
| * [[Mathc matrices/a214| Présentation]] || [[File:Analyse de réseau à l'aide de systèmes linéaires (le problème exemple 3a).png|thumb|]]|| * [[Mathc matrices/a215| Résolution]] || [[File:Analyse de réseau à l'aide de systèmes linéaires (le problème exemple 3b).png|thumb|]]
|}
{{AutoCat}}
1x6o06karbh5zat6rynbg7lftlk7b06
745858
745853
2025-07-03T10:53:48Z
Xhungab
23827
745858
wikitext
text/x-wiki
[[Catégorie:Mathc matrices (livre)]]
[[Mathc matrices/a259| '''Gass-Jordan''']]
{{Partie{{{type|}}}|Analyse de réseaux}}
{| class="wikitable"
|+ Premier exemple : ''Résoudre avec [[Mathc matrices/06g| QR]], [[Mathc matrices/06j| SVD]]
|-
| * [[Mathc matrices/a210| Présentation]] || [[File:Network Analysis Using Linear Systems ex 01a.png|thumb|]]|| * [[Mathc matrices/a211| Résolution]] || [[File:Network Analysis Using Linear Systems ex 01b.png|thumb|]]
|}
{| class="wikitable"
|+ Deuxième exemple : ''Résoudre avec [[Mathc matrices/06h| QR]], [[Mathc matrices/06k| SVD]]
|-
| * [[Mathc matrices/a212| Présentation]] || [[File:Network Analysis Using Linear Systems ex 02a.png|thumb|]]|| * [[Mathc matrices/a213| Résolution]] || [[File:Network Analysis Using Linear Systems ex 02b.png|thumb|]]
|}
{| class="wikitable"
|+ Troisième exemple : ''Résoudre avec [[Mathc matrices/06i| QR]], [[Mathc matrices/06l| SVD]]
|-
| * [[Mathc matrices/a214| Présentation]] || [[File:Analyse de réseau à l'aide de systèmes linéaires (le problème exemple 3a).png|thumb|]]|| * [[Mathc matrices/a215| Résolution]] || [[File:Analyse de réseau à l'aide de systèmes linéaires (le problème exemple 3b).png|thumb|]]
|}
{{AutoCat}}
ndt88npqil2jjv33wysjio6mgjh2req
Mathc matrices/a216
0
80105
745751
725306
2025-07-02T14:35:23Z
Xhungab
23827
745751
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc matrices (livre)]]
[[Mathc matrices/a259| '''Gass-Jordan''']]
{{Partie{{{type|}}}|Analyse d'un circuit électrique}}
{| class="wikitable"
|+ Premier exemple : ''Résoudre avec [[Mathc matrices/06a| QR]]
|-
| * [[Mathc matrices/a217| Présentation]] || [[File:Electrical Networks Using Linear Systems 1a.png|thumb|]]|| * [[Mathc matrices/a218| Résolution]] || [[File:Electrical Networks Using Linear Systems 1b.png|thumb|]]
|}
{| class="wikitable"
|+ Deuxième exemple : ''Résoudre avec [[Mathc matrices/06b| QR]]
|-
| * [[Mathc matrices/a219| Présentation]] || [[File:Electrical Networks Using Linear Systems 2a.png|thumb|]]|| * [[Mathc matrices/a220| Résolution]] || [[File:Electrical Networks Using Linear Systems 2b.png|thumb|]]
|}
{| class="wikitable"
|+ Troisième exemple : ''Résoudre avec [[Mathc matrices/06a| QR]]
|-
| * [[Mathc matrices/a221| Présentation]] || [[File:Electrical Networks Using Linear Systems 3a.png|thumb|]]|| * [[Mathc matrices/a222| Résolution]] || [[File:Electrical Networks Using Linear Systems 3b.png|thumb|]]
|}
{{AutoCat}}
evo3vtuxvqg7q3tgi6qesuqkfqi0ov2
745754
745751
2025-07-02T14:44:17Z
Xhungab
23827
745754
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc matrices (livre)]]
[[Mathc matrices/a259| '''Gass-Jordan''']]
{{Partie{{{type|}}}|Analyse d'un circuit électrique}}
{| class="wikitable"
|+ Premier exemple : ''Résoudre avec [[Mathc matrices/06a| QR]]
|-
| * [[Mathc matrices/a217| Présentation]] || [[File:Electrical Networks Using Linear Systems 1a.png|thumb|]]|| * [[Mathc matrices/a218| Résolution]] || [[File:Electrical Networks Using Linear Systems 1b.png|thumb|]]
|}
{| class="wikitable"
|+ Deuxième exemple : ''Résoudre avec [[Mathc matrices/06b| QR]]
|-
| * [[Mathc matrices/a219| Présentation]] || [[File:Electrical Networks Using Linear Systems 2a.png|thumb|]]|| * [[Mathc matrices/a220| Résolution]] || [[File:Electrical Networks Using Linear Systems 2b.png|thumb|]]
|}
{| class="wikitable"
|+ Troisième exemple : ''Résoudre avec [[Mathc matrices/06c| QR]]
|-
| * [[Mathc matrices/a221| Présentation]] || [[File:Electrical Networks Using Linear Systems 3a.png|thumb|]]|| * [[Mathc matrices/a222| Résolution]] || [[File:Electrical Networks Using Linear Systems 3b.png|thumb|]]
|}
{{AutoCat}}
0ww67wd94krkthrb5nu8nf8n0z50lce
745838
745754
2025-07-02T20:46:21Z
Xhungab
23827
745838
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc matrices (livre)]]
[[Mathc matrices/a259| '''Gass-Jordan''']]
{{Partie{{{type|}}}|Analyse d'un circuit électrique}}
{| class="wikitable"
|+ Premier exemple : ''Résoudre avec [[Mathc matrices/06a| QR]], [[Mathc matrices/06d| SVD]]
|-
| * [[Mathc matrices/a217| Présentation]] || [[File:Electrical Networks Using Linear Systems 1a.png|thumb|]]|| * [[Mathc matrices/a218| Résolution]] || [[File:Electrical Networks Using Linear Systems 1b.png|thumb|]]
|}
{| class="wikitable"
|+ Deuxième exemple : ''Résoudre avec [[Mathc matrices/06b| QR]], [[Mathc matrices/06e| SVD]]
|-
|-
| * [[Mathc matrices/a219| Présentation]] || [[File:Electrical Networks Using Linear Systems 2a.png|thumb|]]|| * [[Mathc matrices/a220| Résolution]] || [[File:Electrical Networks Using Linear Systems 2b.png|thumb|]]
|}
{| class="wikitable"
|+ Troisième exemple : ''Résoudre avec [[Mathc matrices/06c| QR]], [[Mathc matrices/06f| SVD]]
|-
|-
| * [[Mathc matrices/a221| Présentation]] || [[File:Electrical Networks Using Linear Systems 3a.png|thumb|]]|| * [[Mathc matrices/a222| Résolution]] || [[File:Electrical Networks Using Linear Systems 3b.png|thumb|]]
|}
{{AutoCat}}
2ger8cqqauh1dubjz4wxv04f1l8dpkn
Mathc complexes/053
0
82537
745845
745371
2025-07-03T08:04:40Z
Xhungab
23827
745845
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc complexes (livre)]]
:
[[Mathc complexes/a26| '''Gauss-Jordan''']]
:
{{Partie{{{type|}}}|Analyse de réseaux}}
{| class="wikitable"
|+ Premier exemple : ''Résoudre avec [[Mathc complexes/05w| QR]]
|-
| * [[Mathc complexes/059| Présentation]] || [[File:Network Analysis Using Linear Systems ex 01a.png|thumb|]]|| * [[Mathc complexes/054| Résolution]] || [[File:Network Analysis Using Linear Systems ex 01b.png|thumb|]]
|}
{| class="wikitable"
|+ Deuxième exemple : ''Résoudre avec [[Mathc complexes/05x| QR]]
|-
| * [[Mathc complexes/055| Présentation]] || [[File:Network Analysis Using Linear Systems ex 02a.png|thumb|]]|| * [[Mathc complexes/056| Résolution]] || [[File:Network Analysis Using Linear Systems ex 02b.png|thumb|]]
|}
{| class="wikitable"
|+ Troisième exemple : ''Résoudre avec [[Mathc complexes/05y| QR]]
|-
| * [[Mathc complexes/057| Présentation]] || [[File:Analyse de réseau à l'aide de systèmes linéaires (le problème exemple 3a).png|thumb|]]|| * [[Mathc complexes/058| Résolution]] || [[File:Analyse de réseau à l'aide de systèmes linéaires (le problème exemple 3b).png|thumb|]]
|}
{{AutoCat}}
ixb19pdpaml5hsbtw4knxdf91a4iv8w
745849
745845
2025-07-03T08:47:05Z
Xhungab
23827
745849
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc complexes (livre)]]
:
[[Mathc complexes/a26| '''Gauss-Jordan''']]
:
{{Partie{{{type|}}}|Analyse de réseaux}}
{| class="wikitable"
|+ Premier exemple : ''Résoudre avec [[Mathc complexes/05w| QR]], [[Mathc complexes/05z| XVD]]
|-
| * [[Mathc complexes/059| Présentation]] || [[File:Network Analysis Using Linear Systems ex 01a.png|thumb|]]|| * [[Mathc complexes/054| Résolution]] || [[File:Network Analysis Using Linear Systems ex 01b.png|thumb|]]
|}
{| class="wikitable"
|+ Deuxième exemple : ''Résoudre avec [[Mathc complexes/05x| QR]], [[Mathc complexes/060| XVD]]
|-
| * [[Mathc complexes/055| Présentation]] || [[File:Network Analysis Using Linear Systems ex 02a.png|thumb|]]|| * [[Mathc complexes/056| Résolution]] || [[File:Network Analysis Using Linear Systems ex 02b.png|thumb|]]
|}
{| class="wikitable"
|+ Troisième exemple : ''Résoudre avec [[Mathc complexes/05y| QR]], [[Mathc complexes/061| XVD]]
|-
| * [[Mathc complexes/057| Présentation]] || [[File:Analyse de réseau à l'aide de systèmes linéaires (le problème exemple 3a).png|thumb|]]|| * [[Mathc complexes/058| Résolution]] || [[File:Analyse de réseau à l'aide de systèmes linéaires (le problème exemple 3b).png|thumb|]]
|}
{{AutoCat}}
jjpgclruarntz58xdxzmly3xgwofiq2
Mathc complexes/05a
0
82544
745714
745675
2025-07-02T12:55:00Z
Xhungab
23827
745714
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc complexes (livre)]]
:
[[Mathc complexes/a26| '''Gauss-Jordan''']]
:
{{Partie{{{type|}}}|Analyse d'un circuit électrique}}
{| class="wikitable"
|+ Premier exemple : ''Résoudre avec [[Mathc complexes/05r| QR]], [[Mathc complexes/05u| XVD]]''
|-
| * [[Mathc complexes/05b| Présentation]] || [[File:Electrical Networks Using Linear Systems 1a.png|thumb|]]|| * [[Mathc complexes/05c| Résolution]] || [[File:Electrical Networks Using Linear Systems 1b.png|thumb|]]
|}
{| class="wikitable"
|+ Deuxième exemple : ''Résoudre avec [[Mathc complexes/05s| QR]], [[Mathc complexes/05v| XVD]]''
|-
| * [[Mathc complexes/05d| Présentation]] || [[File:Electrical Networks Using Linear Systems 2a.png|thumb|]]|| * [[Mathc complexes/05e| Résolution]] || [[File:Electrical Networks Using Linear Systems 2b.png|thumb|]]
|}
{| class="wikitable"
|+ Troisième exemple : ''Résoudre avec [[Mathc complexes/05t| QR]]''
|-
| * [[Mathc complexes/05f| Présentation]] || [[File:Electrical Networks Using Linear Systems 3a.png|thumb|]]|| * [[Mathc complexes/05g| Résolution]] || [[File:Electrical Networks Using Linear Systems 3b.png|thumb|]]
|}
{{AutoCat}}
3zdbgpyfwhlghq1qm24l1lfkkz2q6m6
Mathc complexes/05u
0
82569
745715
2025-07-02T12:56:56Z
Xhungab
23827
news
745715
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc complexes (livre)]]
:
[[Mathc complexes/05a| '''Application''']]
:
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Electrical Networks Using Linear Systems 1a.png|thumb|]]
{{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Xave as : c00a.c */
/* ------------------------------------ */
#include "w_a.h"
/* ------------------------------------ */
#define RA R4
#define CA C3
#define Cb C1
/* ------------------------------------ */
#define FACTOR_E +1.E-2
/* ------------------------------------ */
/* ------------------------------------ */
int main(void)
{
double ta[RA*(CA*C2)] ={
+1,+0, +1,+0, -1,+0,
-60,+0, +30,+0, +0,+0,
+0,+0, +30,+0, +20,+0,
+60,+0, +0,+0, +20,+0 };
double tb[RA*(Cb*C2)]={
+0, +0,
+0, +0,
+120,+0,
+120,+0
};
double **A = ca_A_mZ(ta,i_mZ(RA,CA));
double **A_T = ctranspose_mZ(A, i_mZ(CA,RA));
double **b = ca_A_mZ(tb, i_mZ(RA,C1));
double **x = i_mZ(CA,C1);
double **V = i_mZ(CA,CA);
double **V_T = i_mZ(CA,CA);
double **U = i_mZ(RA,CA);
double **U_T = i_mZ(CA,RA);
double **U_TA = i_mZ(CA,CA); // CA,RA RA,CA :CA,CA
double **U_TAV = i_mZ(CA,CA); // CA,CA CA,CA :CA,CA
double **invU_TAV = i_mZ(CA,CA); // :CA,CA
double **V_invU_TAV = i_mZ(CA,CA); // CA,CA CA,CA :CA,CA
double **Pinv = i_mZ(CA,RA);
// Pinv = V_invU_TAV * U_T // CA,CA CA,RA :CA,RA
clrscrn();
printf(" A :");
p_mRZ(A, S10,P2, C6);
printf(" b :");
p_mRZ(b, S10,P2, C6);
stop();
clrscrn();
printf(" U :");
X_U_mZ(A_T,U,FACTOR_E);
p_mRZ(U, S10,P4, C6);
printf(" V :");
X_V_mZ(A_T,V,FACTOR_E);
p_mRZ(V, S10,P4, C6);
ctranspose_mZ(U,U_T);
ctranspose_mZ(V,V_T);
stop();
clrscrn();
printf(" U_TAV :");
mul_mZ(U_T, A, U_TA); // U_TA CA,RA RA,CA :CA,CA
mul_mZ(U_TA, V, U_TAV ); // V :CA,CA
p_mRZ(U_TAV, S11,P4, C6); // U_TAV CA,CA CA,CA :CA,CA
printf(" inv(U_TAV) :");
X_inv_mZ(U_TAV, invU_TAV);
pE_mRZ(invU_TAV, S10,P4, C6);
stop();
clrscrn();
printf(" Pinv = V * inv(U_TAV) * U_T:");
mul_mZ(V, invU_TAV, V_invU_TAV);
mul_mZ(V_invU_TAV, U_T, Pinv);
pE_mRZ(Pinv, S13,P4, C6);
stop();
clrscrn();
printf(" A x = b \n"
" Pinv A x = Pinv b \n"
" Ide x = Pinv b \n\n"
" x = Pinv b ");
mul_mZ(Pinv, b, x);
p_mRZ(x, S12,P4, C6);
stop();
f_mZ(A);
f_mZ(A_T);
f_mZ(b);
f_mZ(x);
f_mZ(V);
f_mZ(V_T);
f_mZ(U);
f_mZ(U_T);
f_mZ(U_TA);
f_mZ(invU_TAV);
f_mZ(V_invU_TAV);
f_mZ(Pinv);
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Electrical Networks Using Linear Systems 1b.png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
A :
+1.00 +1.00 -1.00
-60.00 +30.00 +0.00
+0.00 +30.00 +20.00
+60.00 +0.00 +20.00
b :
+0.00
+0.00
+120.00
+120.00
Press return to continue.
U :
+0.0062 +0.0083 -0.9999
-0.7349 +0.3557 -0.0016
-0.0594 +0.8143 +0.0064
+0.6755 +0.4586 +0.0079
V :
+0.9533 +0.1411 -0.2671
-0.2684 +0.8016 -0.5343
+0.1387 +0.5810 +0.8020
Press return to continue.
U_TAV :
+88.7801 +0.0000 +0.0000
-0.0000 +43.8009 -0.0000
-0.0000 -0.0000 +1.6035
inv(U_TAV) :
+1.1264e-02 +0.0000e+00 +0.0000e+00
+0.0000e+00 +2.2831e-02 +0.0000e+00
+0.0000e+00 +0.0000e+00 +6.2364e-01
Press return to continue.
Pinv = V * inv(U_TAV) * U_T:
+1.6667e-01 -6.4815e-03 +9.2593e-04 +7.4074e-03
+3.3333e-01 +9.2593e-03 +1.2963e-02 +3.7037e-03
-5.0000e-01 +2.7778e-03 +1.3889e-02 +1.1111e-02
Press return to continue.
A x = b
Pinv A x = Pinv b
Ide x = Pinv b
x = Pinv b
+1.0000
+2.0000
+3.0000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
6pyiqtbrq0kcjkrl2zl9uxpc2p1p16m
Mathc complexes/05v
0
82570
745717
2025-07-02T13:02:04Z
Xhungab
23827
news
745717
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc complexes (livre)]]
:
[[Mathc complexes/05a| '''Application''']]
:
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Electrical Networks Using Linear Systems 2a.png|thumb|]]
{{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Xave as : c00a.c */
/* ------------------------------------ */
#include "w_a.h"
/* ------------------------------------ */
#define RA R8
#define CA C6
#define Cb C1
/* ------------------------------------ */
#define FACTOR_E +1.E-2
/* ------------------------------------ */
/* ------------------------------------ */
int main(void)
{
double ta[RA*(CA*C2)]={
//I1 I2 I3 I4 I5 I6
+1,+0, -1,+0, -1,+0, +0,+0, +0,+0, +0,+0,
+0,+0, +0,+0, +1,+0, -1,+0, -1,+0, +0,+0,
+0,+0, +1,+0, +0,+0, +0,+0, +1,+0, -1,+0,
-1,+0, +0,+0, +0,+0, +1,+0, +0,+0, +1,+0,
+0,+0, -50,+0, +0,+0, +0,+0, +0,+0, -20,+0,
+0,+0, +50,+0, -20,+0, +0,+0, -10,+0, +0,+0,
+0,+0, +0,+0, +0,+0, -50,+0, +10,+0, +20,+0,
+0,+0, +0,+0, -20,+0, -50,+0, +0,+0, +0,+0,
};
double tb[RA*(Cb*C2)]={
0,+0,
0,+0,
0,+0,
0,+0,
-90,+0,
0,+0,
0,+0,
-90,+0
};
double **A = ca_A_mZ(ta,i_mZ(RA,CA));
double **A_T = ctranspose_mZ(A, i_mZ(CA,RA));
double **b = ca_A_mZ(tb, i_mZ(RA,C1));
double **x = i_mZ(CA,C1);
double **V = i_mZ(CA,CA);
double **V_T = i_mZ(CA,CA);
double **U = i_mZ(RA,CA);
double **U_T = i_mZ(CA,RA);
double **U_TA = i_mZ(CA,CA); // CA,RA RA,CA :CA,CA
double **U_TAV = i_mZ(CA,CA); // CA,CA CA,CA :CA,CA
double **invU_TAV = i_mZ(CA,CA); // :CA,CA
double **V_invU_TAV = i_mZ(CA,CA); // CA,CA CA,CA :CA,CA
double **Pinv = i_mZ(CA,RA);
// Pinv = V_invU_TAV * U_T // CA,CA CA,RA :CA,RA
clrscrn();
printf(" A :");
p_mRZ(A, S8,P2, C6);
printf(" b :");
p_mRZ(b, S8,P2, C6);
stop();
clrscrn();
printf(" U :");
X_U_mZ(A_T,U,FACTOR_E);
p_mRZ(U, S8,P4, C6);
printf(" V :");
X_V_mZ(A_T,V,FACTOR_E);
p_mRZ(V, S8,P4, C6);
ctranspose_mZ(U,U_T);
ctranspose_mZ(V,V_T);
stop();
clrscrn();
printf(" U_TAV :");
mul_mZ(U_T, A, U_TA); // U_TA CA,RA RA,CA :CA,CA
mul_mZ(U_TA, V, U_TAV ); // V :CA,CA
p_mRZ(U_TAV, S11,P4, C6); // U_TAV CA,CA CA,CA :CA,CA
printf(" inv(U_TAV) :");
X_inv_mZ(U_TAV, invU_TAV);
pE_mRZ(invU_TAV, S10,P4, C6);
stop();
clrscrn();
printf(" Pinv = V * inv(U_TAV) * U_T:");
mul_mZ(V, invU_TAV, V_invU_TAV);
mul_mZ(V_invU_TAV, U_T, Pinv);
pE_mRZ(Pinv, S10,P4, C6);
stop();
clrscrn();
printf(" A x = b \n"
" Pinv A x = Pinv b \n"
" Ide x = Pinv b \n\n"
" x = Pinv b ");
mul_mZ(Pinv, b, x);
p_mRZ(x, S10,P4, C6);
stop();
f_mZ(A);
f_mZ(A_T);
f_mZ(b);
f_mZ(x);
f_mZ(V);
f_mZ(V_T);
f_mZ(U);
f_mZ(U_T);
f_mZ(U_TA);
f_mZ(invU_TAV);
f_mZ(V_invU_TAV);
f_mZ(Pinv);
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Electrical Networks Using Linear Systems 2b.png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
A :
+1.00 -1.00 -1.00 +0.00 +0.00 +0.00
+0.00 +0.00 +1.00 -1.00 -1.00 +0.00
+0.00 +1.00 +0.00 +0.00 +1.00 -1.00
-1.00 +0.00 +0.00 +1.00 +0.00 +1.00
+0.00 -50.00 +0.00 +0.00 +0.00 -20.00
+0.00 +50.00 -20.00 +0.00 -10.00 +0.00
+0.00 +0.00 +0.00 -50.00 +10.00 +20.00
+0.00 +0.00 -20.00 -50.00 +0.00 +0.00
b :
+0.00
+0.00
+0.00
+0.00
-90.00
+0.00
+0.00
-90.00
Press return to continue.
U :
-0.0052 +0.0097 +0.0236 +0.5000 -0.1924 -0.6800
+0.0052 +0.0079 -0.0101 -0.5000 +0.6802 -0.1927
+0.0052 -0.0079 +0.0101 -0.5000 -0.6802 +0.1927
-0.0052 -0.0097 -0.0236 +0.5000 +0.1924 +0.6800
-0.5000 +0.4879 +0.5112 -0.0052 +0.0083 +0.0224
+0.5000 -0.5116 +0.4878 +0.0052 +0.0147 +0.0055
+0.5000 +0.5116 -0.4878 +0.0052 -0.0147 -0.0055
+0.5000 +0.4879 +0.5112 +0.0052 +0.0083 +0.0224
V :
+0.0000 +0.0003 -0.0016 +0.0000 -0.2124 +0.9772
+0.6566 -0.6998 +0.0395 -0.2624 -0.0913 -0.0196
-0.2624 +0.0066 +0.6676 -0.6566 +0.2274 +0.0505
-0.6566 -0.6998 +0.0395 +0.2624 -0.0913 -0.0196
+0.0000 +0.1430 +0.3248 +0.0000 -0.9137 -0.1981
+0.2624 +0.0066 +0.6676 +0.6566 +0.2274 +0.0505
Press return to continue.
U_TAV :
+76.1618 +0.0000 -0.0000 +0.0000 -0.0000 -0.0000
+0.0000 +71.4421 +0.0000 +0.0000 +0.0000 -0.0000
-0.0000 -0.0000 -29.9801 -0.0000 -0.0000 +0.0000
-0.0000 -0.0000 +0.0000 +1.8382 +0.0000 +0.0000
-0.0000 -0.0000 -0.0000 +0.0000 +1.8116 +0.0000
+0.0000 -0.0000 -0.0000 +0.0000 -0.0000 -1.3917
inv(U_TAV) :
+1.3130e-02 +0.0000e+00 +0.0000e+00 +0.0000e+00 +0.0000e+00 +0.0000e+00
+0.0000e+00 +1.3997e-02 +0.0000e+00 +0.0000e+00 +0.0000e+00 +0.0000e+00
+0.0000e+00 +0.0000e+00 -3.3355e-02 +0.0000e+00 +0.0000e+00 +0.0000e+00
+0.0000e+00 +0.0000e+00 +0.0000e+00 +5.4401e-01 +0.0000e+00 +0.0000e+00
+0.0000e+00 +0.0000e+00 +0.0000e+00 +0.0000e+00 +5.5198e-01 +0.0000e+00
+0.0000e+00 +0.0000e+00 +0.0000e+00 +0.0000e+00 +0.0000e+00 -7.1857e-01
Press return to continue.
Pinv = V * inv(U_TAV) * U_T:
+5.0000e-01 +5.5556e-02 -5.5556e-02 -5.0000e-01 -1.6667e-02 -5.5556e-03
-7.1429e-02 +3.4392e-02 +1.0847e-01 -7.1429e-02 -9.1270e-03 +7.2751e-03
-1.7857e-01 +2.7116e-01 +8.5979e-02 -1.7857e-01 -7.5397e-03 -1.2831e-02
+7.1429e-02 -1.0847e-01 -3.4392e-02 +7.1429e-02 -1.9841e-03 +1.3228e-04
-8.6245e-12 -3.7037e-01 +3.7037e-01 -7.2615e-12 -5.5556e-03 -1.2963e-02
+1.7857e-01 -8.5979e-02 -2.7116e-01 +1.7857e-01 -1.4683e-02 -5.6878e-03
+5.5556e-03 -1.6667e-02
-1.3228e-04 -1.9841e-03
+5.6878e-03 -1.4683e-02
-7.2751e-03 -9.1270e-03
+1.2963e-02 -5.5556e-03
+1.2831e-02 -7.5397e-03
Press return to continue.
A x = b
Pinv A x = Pinv b
Ide x = Pinv b
x = Pinv b
+3.0000
+1.0000
+2.0000
+1.0000
+1.0000
+2.0000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
g8y18oc4d67zs69tf0pt0og4ecczmit
Mathc matrices/06a
0
82571
745752
2025-07-02T14:38:21Z
Xhungab
23827
news
745752
wikitext
text/x-wiki
[[Catégorie:Mathc matrices (livre)]]
[[Mathc matrices/a216| '''Application''']]
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Electrical Networks Using Linear Systems 1a.png|thumb|]]
{{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Save as : c00a.c */
/* ------------------------------------ */
#include "v_a.h"
/* ------------------------------------ */
/* ------------------------------------ */
#define RA R4
#define CA C3
#define Cb C1
/* ------------------------------------ */
/* ------------------------------------ */
int main(void)
{
double ta[RA*(CA+Cb)]={
// I1 I2 I3
+1, +1, -1,
-60, +30, +0,
+0, +30, +20,
+60, +0, +20,
};
double tb[RA*(CA+Cb)]={
// I1 I2 I3
+0,
+0,
+120,
+120
};
double **A = ca_A_mR(ta,i_mR(RA,CA));
double **b = ca_A_mR(tb,i_mR(RA,Cb));
double **Q = i_mR(RA,CA);
double **Q_T = i_mR(CA,RA);
double **R = i_mR(CA,CA);
double **invR = i_mR(CA,CA);
double **invR_Q_T = i_mR(CA,RA);
double **x = i_mR(CA,C1);
clrscrn();
printf(" Copy/Past into the octave windows \n\n");
p_Octave_mR(A,"a",P0);
printf(" [Q, R] = qr (a,0) \n\n");
QR_mR(A,Q,R);
printf(" Q :");
p_mR(Q, S10,P4, C10);
printf(" R :");
p_mR(R, S10,P4, C10);
stop();
clrscrn();
transpose_mR(Q,Q_T);
printf(" Q_T :");
pE_mR(Q_T,S9,P5, C3);
inv_mR(R,invR);
printf(" invR :");
pE_mR(invR,S9,P5, C6);
stop();
clrscrn();
printf(" Solving this system yields a unique\n"
" least squares solution, namely \n\n");
mul_mR(invR,Q_T,invR_Q_T);
mul_mR(invR_Q_T,b,x);
printf(" x = invR * Q_T * b :");
p_mR(x,S9,P5 ,C6);
stop();
f_mR(A);
f_mR(b);
f_mR(Q);
f_mR(Q_T);
f_mR(R);
f_mR(invR);
f_mR(x);
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Electrical Networks Using Linear Systems 1b.png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
Copy/Past into the octave windows
a=[
+1,+1,-1;
-60,+30,+0;
+0,+30,+20;
+60,+0,+20]
[Q, R] = qr (a,0)
Q :
+0.0118 +0.0340 -0.9994
-0.7071 +0.4083 +0.0056
+0.0000 +0.8160 +0.0278
+0.7071 +0.4077 +0.0222
R :
+84.8587 -21.1999 +14.1294
+0.0000 +36.7636 +24.4411
-0.0000 +0.0000 +1.9987
Press return to continue.
Q_T :
+1.17843e-02 -7.07058e-01 +0.00000e+00
+3.39963e-02 +4.08296e-01 +8.16025e-01
-9.99352e-01 +5.55196e-03 +2.77598e-02
+7.07058e-01
+4.07729e-01
+2.22078e-02
invR :
+1.17843e-02 +6.79548e-03 -1.66405e-01
+0.00000e+00 +2.72008e-02 -3.32624e-01
-0.00000e+00 -0.00000e+00 +5.00324e-01
Press return to continue.
Solving this system yields a unique
least squares solution, namely
x = invR * Q_T * b :
+1.00000
+2.00000
+3.00000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
1pq56yvnowh6ahac713gz0i499o1gfn
Mathc matrices/06b
0
82572
745753
2025-07-02T14:41:38Z
Xhungab
23827
news
745753
wikitext
text/x-wiki
[[Catégorie:Mathc matrices (livre)]]
[[Mathc matrices/a216| '''Application''']]
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Electrical Networks Using Linear Systems 2a.png|thumb|]]
{{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Save as : c00a.c */
/* ------------------------------------ */
#include "v_a.h"
/* ------------------------------------ */
/* ------------------------------------ */
#define RA R8
#define CA C6
#define Cb C1
/* ------------------------------------ */
int main(void)
{
double ta[RA*(CA+Cb)]={
// I1 I2 I3 I4 I5 I6
+1, -1, -1, +0, +0, +0,
+0, +0, +1, -1, -1, +0,
+0, +1, +0, +0, +1, -1,
-1, +0, +0, +1, +0, +1,
+0, -50, +0, +0, +0, -20,
+0, +50, -20, +0, -10, +0,
+0, +0, +0, -50, +10, +20,
+0, +0, -20, -50, +0, +0,
};
double tb[RA*(CA+Cb)]={
0,
0,
0,
0,
-90,
0,
0,
-90,
};
double **A = ca_A_mR(ta,i_mR(RA,CA));
double **b = ca_A_mR(tb,i_mR(RA,Cb));
double **Q = i_mR(RA,CA);
double **Q_T = i_mR(CA,RA);
double **R = i_mR(CA,CA);
double **invR = i_mR(CA,CA);
double **invR_Q_T = i_mR(CA,RA);
double **x = i_mR(CA,C1);
clrscrn();
printf(" Copy/Past into the octave windows \n\n");
p_Octave_mR(A,"a",P0);
printf(" [Q, R] = qr (a,0) \n\n");
stop();
clrscrn();
QR_mR(A,Q,R);
printf(" Q :");
p_mR(Q, S10,P4, C10);
printf(" R :");
p_mR(R, S10,P4, C10);
stop();
clrscrn();
transpose_mR(Q,Q_T);
printf(" Q_T :");
pE_mR(Q_T,S9,P3, C6);
inv_mR(R,invR);
printf(" invR :");
pE_mR(invR,S9,P3, C6);
stop();
clrscrn();
printf(" Solving this system yields a unique\n"
" least squares solution, namely \n\n");
mul_mR(invR,Q_T,invR_Q_T);
mul_mR(invR_Q_T,b,x);
printf(" x = invR * Q_T * b :");
p_mR(x,S9,P4 ,C6);
stop();
f_mR(A);
f_mR(b);
f_mR(Q);
f_mR(Q_T);
f_mR(R);
f_mR(invR);
f_mR(x);
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Electrical Networks Using Linear Systems 2b.png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
Copy/Past into the octave windows
a=[
+1,-1,-1,+0,+0,+0;
+0,+0,+1,-1,-1,+0;
+0,+1,+0,+0,+1,-1;
-1,+0,+0,+1,+0,+1;
+0,-50,+0,+0,+0,-20;
+0,+50,-20,+0,-10,+0;
+0,+0,+0,-50,+10,+20;
+0,+0,-20,-50,+0,+0]
[Q, R] = qr (a,0)
Press return to continue.
Q :
+0.7071 -0.0071 -0.0245 +0.0259 +0.1704 +0.4687
+0.0000 +0.0000 +0.0408 -0.0460 -0.8338 -0.2257
+0.0000 +0.0141 +0.0081 -0.0057 +0.4931 -0.7117
-0.7071 -0.0071 -0.0245 +0.0259 +0.1704 +0.4687
+0.0000 -0.7070 -0.4073 +0.2866 -0.0253 -0.0385
+0.0000 +0.7070 -0.4080 +0.2872 -0.0317 -0.0149
+0.0000 +0.0000 +0.0000 -0.8645 +0.0386 +0.0337
+0.0000 +0.0000 -0.8153 -0.2908 -0.0185 -0.0198
R :
+1.4142 -0.7071 -0.7071 -0.7071 +0.0000 -0.7071
+0.0000 +70.7213 -14.1329 -0.0071 -7.0559 +14.1188
+0.0000 -0.0000 +24.5308 +40.6999 +4.0472 +8.1139
+0.0000 +0.0000 +0.0000 +57.8362 -11.4767 -22.9897
+0.0000 +0.0000 +0.0000 -0.0000 +2.0299 +0.9540
+0.0000 -0.0000 +0.0000 -0.0000 -0.0000 +2.6246
Press return to continue.
Q_T :
+7.071e-01 +0.000e+00 +0.000e+00 -7.071e-01 +0.000e+00 +0.000e+00
-7.070e-03 +0.000e+00 +1.414e-02 -7.070e-03 -7.070e-01 +7.070e-01
-2.446e-02 +4.077e-02 +8.146e-03 -2.446e-02 -4.073e-01 -4.080e-01
+2.585e-02 -4.598e-02 -5.731e-03 +2.585e-02 +2.866e-01 +2.872e-01
+1.704e-01 -8.338e-01 +4.931e-01 +1.704e-01 -2.528e-02 -3.174e-02
+4.687e-01 -2.257e-01 -7.117e-01 +4.687e-01 -3.854e-02 -1.493e-02
+0.000e+00 +0.000e+00
+0.000e+00 +0.000e+00
+0.000e+00 -8.153e-01
-8.645e-01 -2.908e-01
+3.855e-02 -1.847e-02
+3.368e-02 -1.979e-02
invR :
+7.071e-01 +7.070e-03 +2.446e-02 -8.564e-03 -7.260e-02 +2.824e-02
+0.000e+00 +1.414e-02 +8.146e-03 -5.731e-03 +5.057e-04 -1.516e-01
+0.000e+00 +0.000e+00 +4.077e-02 -2.869e-02 -2.435e-01 -2.888e-01
-0.000e+00 -0.000e+00 -0.000e+00 +1.729e-02 +9.775e-02 +1.159e-01
-0.000e+00 -0.000e+00 -0.000e+00 +0.000e+00 +4.926e-01 -1.791e-01
-0.000e+00 -0.000e+00 -0.000e+00 +0.000e+00 -0.000e+00 +3.810e-01
Press return to continue.
Solving this system yields a unique
least squares solution, namely
x = invR * Q_T * b :
+3.0000
+1.0000
+2.0000
+1.0000
+1.0000
+2.0000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
gvewh6g5ngtxe7r81emmpcgrj9du1t3
Mathc matrices/06c
0
82573
745755
2025-07-02T14:44:49Z
Xhungab
23827
news
745755
wikitext
text/x-wiki
[[Catégorie:Mathc matrices (livre)]]
[[Mathc matrices/a216| '''Application''']]
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Electrical Networks Using Linear Systems 3a.png|thumb|]]
{{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Save as : c00a.c */
/* ------------------------------------ */
#include "v_a.h"
/* ------------------------------------ */
/* ------------------------------------ */
#define RA R8
#define CA C6
#define Cb C1
/* ------------------------------------ */
int main(void)
{
double ta[RA*(CA+Cb)]={
// I1 I2 I3 I4 I5 I6
-1, +1, +1, +0, +0, +0,
+0, +0, -1, +1, -1, +0,
+0, +0, +0, -1, +1, +1,
+1, -1, +0, +0, +0, -1,
+15, +60, +0, +0, +0, +0,
+0, -60, +15, +15, +0, +15,
+0, +0, +0, -15, -60, +0,
+15, +0, +15, +0, -60, +15
};
double tb[RA*(CA+Cb)]={
+0,
+0,
+0,
+0,
+90,
+0,
-90,
+0
};
double **A = ca_A_mR(ta,i_mR(RA,CA));
double **b = ca_A_mR(tb,i_mR(RA,Cb));
double **Q = i_mR(RA,CA);
double **Q_T = i_mR(CA,RA);
double **R = i_mR(CA,CA);
double **invR = i_mR(CA,CA);
double **invR_Q_T = i_mR(CA,RA);
double **x = i_mR(CA,C1);
clrscrn();
printf(" Copy/Past into the octave windows \n\n");
p_Octave_mR(A,"a",P0);
printf(" [Q, R] = qr (a,0) \n\n");
stop();
clrscrn();
QR_mR(A,Q,R);
printf(" Q :");
p_mR(Q, S10,P4, C10);
printf(" R :");
p_mR(R, S10,P4, C10);
stop();
clrscrn();
transpose_mR(Q,Q_T);
printf(" Q_T :");
pE_mR(Q_T,S9,P3, C6);
inv_mR(R,invR);
printf(" invR :");
pE_mR(invR,S9,P3, C6);
stop();
clrscrn();
printf(" Solving this system yields a unique\n"
" least squares solution, namely \n\n");
mul_mR(invR,Q_T,invR_Q_T);
mul_mR(invR_Q_T,b,x);
printf(" x = invR * Q_T * b :");
p_mR(x,S9,P4 ,C6);
stop();
f_mR(A);
f_mR(b);
f_mR(Q);
f_mR(Q_T);
f_mR(R);
f_mR(invR);
f_mR(x);
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Electrical Networks Using Linear Systems 3b.png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
Copy/Past into the octave windows
a=[
-1,+1,+1,+0,+0,+0;
+0,+0,-1,+1,-1,+0;
+0,+0,+0,-1,+1,+1;
+1,-1,+0,+0,+0,-1;
+15,+60,+0,+0,+0,+0;
+0,-60,+15,+15,+0,+15;
+0,+0,+0,-15,-60,+0;
+15,+0,+15,+0,-60,+15]
[Q, R] = qr (a,0)
Press return to continue.
Q :
-0.0470 +0.0406 +0.8125 +0.0160 -0.1464 -0.2537
+0.0000 +0.0000 -0.3633 +0.0631 -0.7414 +0.2537
+0.0000 +0.0000 +0.0000 -0.0575 +0.5581 +0.6597
+0.0470 -0.0406 -0.4492 -0.0216 +0.3297 -0.6597
+0.7055 +0.4103 +0.0210 +0.2881 +0.0177 +0.0101
+0.0000 -0.8151 +0.0421 +0.2888 +0.0098 +0.0169
+0.0000 +0.0000 +0.0000 -0.8625 -0.0769 -0.0101
+0.7055 -0.4049 +0.0631 -0.2856 -0.0494 +0.0169
R :
+21.2603 +42.2384 +10.5361 +0.0000 -42.3324 +10.5361
-0.0000 +73.6065 -18.2596 -12.2272 +24.2920 -18.2596
-0.0000 -0.0000 +2.7528 +0.2675 -3.4216 +2.0262
-0.0000 -0.0000 -0.0000 +17.3904 +68.7702 +0.0112
-0.0000 -0.0000 +0.0000 -0.0000 +8.8776 -0.3666
-0.0000 -0.0000 -0.0000 +0.0000 +0.0000 +1.8269
Press return to continue.
Q_T :
-4.704e-02 +0.000e+00 +0.000e+00 +4.704e-02 +7.055e-01 +0.000e+00
+4.058e-02 +0.000e+00 +0.000e+00 -4.058e-02 +4.103e-01 -8.151e-01
+8.125e-01 -3.633e-01 +0.000e+00 -4.492e-01 +2.103e-02 +4.205e-02
+1.603e-02 +6.309e-02 -5.750e-02 -2.162e-02 +2.881e-01 +2.888e-01
-1.464e-01 -7.414e-01 +5.581e-01 +3.297e-01 +1.769e-02 +9.757e-03
-2.537e-01 +2.537e-01 +6.597e-01 -6.597e-01 +1.015e-02 +1.692e-02
+0.000e+00 +7.055e-01
+0.000e+00 -4.049e-01
+0.000e+00 +6.308e-02
-8.625e-01 -2.856e-01
-7.688e-02 -4.943e-02
-1.015e-02 +1.692e-02
invR :
+4.704e-02 -2.699e-02 -3.591e-01 -1.345e-02 +2.640e-01 -8.974e-02
-0.000e+00 +1.359e-02 +9.012e-02 +8.166e-03 -6.570e-02 +2.260e-02
+0.000e+00 -0.000e+00 +3.633e-01 -5.589e-03 +1.833e-01 -3.661e-01
+0.000e+00 -0.000e+00 +0.000e+00 +5.750e-02 -4.454e-01 -8.974e-02
-0.000e+00 +0.000e+00 -0.000e+00 -0.000e+00 +1.126e-01 +2.260e-02
-0.000e+00 +0.000e+00 -0.000e+00 -0.000e+00 +0.000e+00 +5.474e-01
Press return to continue.
Solving this system yields a unique
least squares solution, namely
x = invR * Q_T * b :
+2.0000
+1.0000
+1.0000
+2.0000
+1.0000
+1.0000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
nz66auyibzf7uxcuy9gnlyhm480a07b
Mathc matrices/06d
0
82575
745841
2025-07-02T20:48:38Z
Xhungab
23827
news
745841
wikitext
text/x-wiki
[[Catégorie:Mathc matrices (livre)]]
[[Mathc matrices/a216| '''Application''']]
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Electrical Networks Using Linear Systems 1a.png|thumb|]]
{{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Save as : c00a.c */
/* ------------------------------------ */
#include "v_a.h"
/* ------------------------------------ */
/* ------------------------------------ */
#define RA R4
#define CA C3
#define Cb C1
/* ------------------------------------ */
#define FACTOR_E +1.E-0
/* ------------------------------------ */
void fun(void)
{
double ta[RA*(CA+Cb)]={
// I1 I2 I3
+1, +1, -1,
-60, +30, +0,
+0, +30, +20,
+60, +0, +20,
};
double tb[RA*(CA+Cb)]={
// I1 I2 I3
+0,
+0,
+120,
+120
};
double **A = ca_A_mR(ta,i_mR(RA,CA));
double **b = ca_A_mR(tb,i_mR(RA,C1));
double **Pinv = i_mR(CA,RA);
double **x = i_mR(CA,C1);
clrscrn();
printf(" A :");
p_mR(A,S5,P1,C6);
printf(" b :");
p_mR(b,S5,P1,C6);
printf(" Pinv = V * invS_T * U_T ");
Pinv_Rn_mR(A,Pinv,FACTOR_E);
pE_mR(Pinv,S12,P4,C6);
printf(" x = Pinv * b ");
mul_mR(Pinv,b,x);
p_mR(x,S10,P4,C6);
stop();
f_mR(b);
f_mR(A);
f_mR(Pinv);
f_mR(x);
}
/* ------------------------------------ */
int main(void)
{
fun();
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Electrical Networks Using Linear Systems 1b.png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
A :
+1.0 +1.0 -1.0
-60.0 +30.0 +0.0
+0.0 +30.0 +20.0
+60.0 +0.0 +20.0
b :
+0.0
+0.0
+120.0
+120.0
Pinv = V * invS_T * U_T
+1.6667e-01 -6.4815e-03 +9.2593e-04 +7.4074e-03
+3.3333e-01 +9.2593e-03 +1.2963e-02 +3.7037e-03
-5.0000e-01 +2.7778e-03 +1.3889e-02 +1.1111e-02
x = Pinv * b
+1.0000
+2.0000
+3.0000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
sm11vwashuo215x1wjw0ag8qxa2j997
Mathc matrices/06e
0
82576
745843
2025-07-02T20:54:18Z
Xhungab
23827
news
745843
wikitext
text/x-wiki
[[Catégorie:Mathc matrices (livre)]]
[[Mathc matrices/a216| '''Application''']]
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Electrical Networks Using Linear Systems 2a.png|thumb|]]
{{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Save as : c00a.c */
/* ------------------------------------ */
#include "v_a.h"
/* ------------------------------------ */
/* ------------------------------------ */
#define RA R8
#define CA C6
#define Cb C1
/* ------------------------------------ */
#define FACTOR_E +1.E-1
/* ------------------------------------ */
void fun(void)
{
double ta[RA*(CA+Cb)]={
// I1 I2 I3 I4 I5 I6
+1, -1, -1, +0, +0, +0,
+0, +0, +1, -1, -1, +0,
+0, +1, +0, +0, +1, -1,
-1, +0, +0, +1, +0, +1,
+0, -50, +0, +0, +0, -20,
+0, +50, -20, +0, -10, +0,
+0, +0, +0, -50, +10, +20,
+0, +0, -20, -50, +0, +0,
};
double tb[RA*(CA+Cb)]={
0,
0,
0,
0,
-90,
0,
0,
-90,
};
double **A = ca_A_mR(ta,i_mR(RA,CA));
double **b = ca_A_mR(tb,i_mR(RA,C1));
double **Pinv = i_mR(CA,RA);
double **x = i_mR(CA,C1);
clrscrn();
printf(" A :");
p_mR(A,S5,P1,C6);
printf(" b :");
p_mR(b,S5,P1,C6);
stop();
clrscrn();
printf(" Pinv = V * invS_T * U_T ");
Pinv_Rn_mR(A,Pinv,FACTOR_E);
pE_mR(Pinv,S12,P3,C5);
printf(" x = Pinv * b ");
mul_mR(Pinv,b,x);
p_mR(x,S10,P3,C5);
stop();
f_mR(b);
f_mR(A);
f_mR(Pinv);
f_mR(x);
}
/* ------------------------------------ */
int main(void)
{
fun();
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Electrical Networks Using Linear Systems 2b.png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
A :
+1.0 -1.0 -1.0 +0.0 +0.0 +0.0
+0.0 +0.0 +1.0 -1.0 -1.0 +0.0
+0.0 +1.0 +0.0 +0.0 +1.0 -1.0
-1.0 +0.0 +0.0 +1.0 +0.0 +1.0
+0.0 -50.0 +0.0 +0.0 +0.0 -20.0
+0.0 +50.0 -20.0 +0.0 -10.0 +0.0
+0.0 +0.0 +0.0 -50.0 +10.0 +20.0
+0.0 +0.0 -20.0 -50.0 +0.0 +0.0
b :
+0.0
+0.0
+0.0
+0.0
-90.0
+0.0
+0.0
-90.0
Press return to continue.
Pinv = V * invS_T * U_T
+5.000e-01 +5.556e-02 -5.556e-02 -5.000e-01 -1.667e-02
-7.143e-02 +3.439e-02 +1.085e-01 -7.143e-02 -9.127e-03
-1.786e-01 +2.712e-01 +8.598e-02 -1.786e-01 -7.540e-03
+7.143e-02 -1.085e-01 -3.439e-02 +7.143e-02 -1.984e-03
+5.755e-11 -3.704e-01 +3.704e-01 +6.044e-11 -5.556e-03
+1.786e-01 -8.598e-02 -2.712e-01 +1.786e-01 -1.468e-02
-5.556e-03 +5.556e-03 -1.667e-02
+7.275e-03 -1.323e-04 -1.984e-03
-1.283e-02 +5.688e-03 -1.468e-02
+1.323e-04 -7.275e-03 -9.127e-03
-1.296e-02 +1.296e-02 -5.556e-03
-5.688e-03 +1.283e-02 -7.540e-03
x = Pinv * b
+3.000
+1.000
+2.000
+1.000
+1.000
+2.000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
e1lr0bk023e9u6507vzv8peq2i3uy57
Mathc matrices/06f
0
82577
745844
2025-07-02T20:57:16Z
Xhungab
23827
news
745844
wikitext
text/x-wiki
[[Catégorie:Mathc matrices (livre)]]
[[Mathc matrices/a216| '''Application''']]
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Electrical Networks Using Linear Systems 3a.png|thumb|]]
{{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Save as : c00a.c */
/* ------------------------------------ */
#include "v_a.h"
/* ------------------------------------ */
/* ------------------------------------ */
#define RA R8
#define CA C6
#define Cb C1
/* ------------------------------------ */
#define FACTOR_E +1.E-1
/* ------------------------------------ */
void fun(void)
{
double ta[RA*(CA+Cb)]={
// I1 I2 I3 I4 I5 I6
-1, +1, +1, +0, +0, +0,
+0, +0, -1, +1, -1, +0,
+0, +0, +0, -1, +1, +1,
+1, -1, +0, +0, +0, -1,
+15, +60, +0, +0, +0, +0,
+0, -60, +15, +15, +0, +15,
+0, +0, +0, -15, -60, +0,
+15, +0, +15, +0, -60, +15
};
double tb[RA*(CA+Cb)]={
+0,
+0,
+0,
+0,
+90,
+0,
-90,
+0
};
double **A = ca_A_mR(ta,i_mR(RA,CA));
double **b = ca_A_mR(tb,i_mR(RA,C1));
double **Pinv = i_mR(CA,RA);
double **x = i_mR(CA,C1);
clrscrn();
printf(" A :");
p_mR(A,S5,P1,C6);
printf(" b :");
p_mR(b,S5,P1,C6);
stop();
clrscrn();
printf(" Pinv = V * invS_T * U_T ");
Pinv_Rn_mR(A,Pinv,FACTOR_E);
pE_mR(Pinv,S12,P4,C5);
printf(" x = Pinv * b ");
mul_mR(Pinv,b,x);
p_mR(x,S10,P4,C5);
stop();
f_mR(b);
f_mR(A);
f_mR(Pinv);
f_mR(x);
}
/* ------------------------------------ */
int main(void)
{
fun();
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Electrical Networks Using Linear Systems 3b.png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
A :
-1.0 +1.0 +1.0 +0.0 +0.0 +0.0
+0.0 +0.0 -1.0 +1.0 -1.0 +0.0
+0.0 +0.0 +0.0 -1.0 +1.0 +1.0
+1.0 -1.0 +0.0 +0.0 +0.0 -1.0
+15.0 +60.0 +0.0 +0.0 +0.0 +0.0
+0.0 -60.0 +15.0 +15.0 +0.0 +15.0
+0.0 +0.0 +0.0 -15.0 -60.0 +0.0
+15.0 +0.0 +15.0 +0.0 -60.0 +15.0
b :
+0.0
+0.0
+0.0
+0.0
+90.0
+0.0
-90.0
+0.0
Press return to continue.
Pinv = V * invS_T * U_T
-3.1111e-01 -8.8889e-02 +8.8889e-02 +3.1111e-01 +1.4444e-02
+7.7778e-02 +2.2222e-02 -2.2222e-02 -7.7778e-02 +8.8889e-03
+3.6111e-01 -3.6111e-01 -1.3889e-01 +1.3889e-01 +5.5556e-03
+8.8889e-02 +3.1111e-01 -3.1111e-01 -8.8889e-02 +7.7778e-03
-2.2222e-02 -7.7778e-02 +7.7778e-02 +2.2222e-02 +2.2222e-03
-1.3889e-01 +1.3889e-01 +3.6111e-01 -3.6111e-01 +5.5556e-03
+4.0741e-03 -7.7778e-03 +1.0741e-02
-5.1852e-03 -2.2222e-03 +1.4815e-03
+9.2593e-03 -5.5556e-03 +9.2593e-03
+1.0741e-02 -1.4444e-02 +4.0741e-03
+1.4815e-03 -8.8889e-03 -5.1852e-03
+9.2593e-03 -5.5556e-03 +9.2593e-03
x = Pinv * b
+2.0000
+1.0000
+1.0000
+2.0000
+1.0000
+1.0000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
d9s8hibmanayqvebgrb56x81otxf6t2
Mathc complexes/05w
0
82578
745846
2025-07-03T08:08:59Z
Xhungab
23827
news
745846
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc complexes (livre)]]
:
[[Mathc complexes/053| '''Application''']]
:
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Network Analysis Using Linear Systems ex 01a.png|thumb|]]
{{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Save as : c00a.c */
/* ------------------------------------ */
#include "w_a.h"
/* ------------------------------------ */
/* ------------------------------------ */
#define RA R4
#define CA C3
#define Cb C1
/* ------------------------------------ */
/* ------------------------------------ */
int main(void)
{
double ta[RA*(CA*C2+Cb*C2)]={
// x1 x2 x3
+1,+0, +1,+0, +0,+0, // A
+0,+0, -1,+0, -1,+0, // B
+0,+0, +0,+0, +1,+0, // C
-1,+0, +0,+0, +0,+0, // D
};
double tb[RA*(CA*C2+Cb*C2)]={
+50, +0, // A
-40, +0, // B
+20 -10,+0, // C
-30 +10 +0 // D
};
double **A = ca_A_mZ(ta,i_mZ(RA,CA));
double **b = ca_A_mZ(tb,i_mZ(RA,Cb));
double **Q = i_mZ(RA,CA);
double **Q_T = i_mZ(CA,RA);
double **R = i_mZ(CA,CA);
double **invR = i_mZ(CA,CA);
double **invR_Q_T = i_mZ(CA,RA);
double **x = i_mZ(CA,C1);
clrscrn();
printf(" Copy/Past into the octave windows \n\n");
p_Octave_mZ(A,"a",P0,P0);
printf(" [Q, R] = qr (a,0) \n\n");
QR_mZ(A,Q,R);
printf(" Q :");
p_mRZ(Q, S10,P4, C10);
printf(" R :");
p_mRZ(R, S10,P4, C10);
stop();
clrscrn();
ctranspose_mZ(Q,Q_T);
printf(" Q_T :");
pE_mRZ(Q_T,S9,P5, C3);
inv_mZ(R,invR);
printf(" invR :");
pE_mRZ(invR,S9,P5, C6);
stop();
clrscrn();
printf(" Solving this system yields a unique\n"
" least squares solution, namely \n\n");
mul_mZ(invR,Q_T,invR_Q_T);
mul_mZ(invR_Q_T,b,x);
printf(" x = invR * Q_T * b :");
p_mRZ(x,S9,P5 ,C6);
stop();
f_mZ(A);
f_mZ(b);
f_mZ(Q);
f_mZ(Q_T);
f_mZ(R);
f_mZ(invR);
f_mZ(x);
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Network Analysis Using Linear Systems ex 01b.png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
Copy/Past into the octave windows
a=[
+1+0*i,+1+0*i,+0+0*i;
+0+0*i,-1+0*i,-1+0*i;
+0+0*i,+0+0*i,+1+0*i;
-1+0*i,+0+0*i,+0+0*i]
[Q, R] = qr (a,0)
Q :
+0.7071 +0.4082 -0.2887
+0.0000 -0.8165 -0.2887
+0.0000 +0.0000 +0.8660
-0.7071 +0.4082 -0.2887
R :
+1.4142 +0.7071 +0.0000
+0.0000 +1.2247 +0.8165
+0.0000 +0.0000 +1.1547
Press return to continue.
Q_T :
+7.07107e-01 +0.00000e+00 +0.00000e+00
+4.08248e-01 -8.16497e-01 +0.00000e+00
-2.88675e-01 -2.88675e-01 +8.66025e-01
-7.07107e-01
+4.08248e-01
-2.88675e-01
invR :
+7.07107e-01 -4.08248e-01 +2.88675e-01
-0.00000e+00 +8.16497e-01 -5.77350e-01
+0.00000e+00 -0.00000e+00 +8.66025e-01
Press return to continue.
Solving this system yields a unique
least squares solution, namely
x = invR * Q_T * b :
+20.00000
+30.00000
+10.00000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
lkpo4zff0c5pt773o3ihhglnr9yh3ob
Mathc complexes/05x
0
82579
745847
2025-07-03T08:12:01Z
Xhungab
23827
news
745847
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc complexes (livre)]]
:
[[Mathc complexes/053| '''Application''']]
:
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Network Analysis Using Linear Systems ex 02a.png|thumb|]]
{{Fichier|c00b.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Save as : c00b.c */
/* ------------------------------------ */
#include "w_a.h"
/* ------------------------------------ */
/* ------------------------------------ */
#define RA R4
#define CA C3
#define Cb C1
/* ------------------------------------ */
/* ------------------------------------ */
int main(void)
{
double ta[RA*(CA*C2+Cb*C2)]={
// x1 x3 x4
+1,+0, +0,+0, +0,+0, // A
+0,+0, +1,+0, +0,+0, // B
+0,+0, -1,+0, +1,+0, // C
-1,+0, +0,+0, -1,+0 // D
};
double tb[RA*(CA*C2+Cb*C2)]={
+20+10, +0, // A
+60-10-30,+0, // B
+20, +0, // C
-100+30, +0 // D
};
double **A = ca_A_mZ(ta,i_mZ(RA,CA));
double **b = ca_A_mZ(tb,i_mZ(RA,Cb));
double **Q = i_mZ(RA,CA);
double **Q_T = i_mZ(CA,RA);
double **R = i_mZ(CA,CA);
double **invR = i_mZ(CA,CA);
double **invR_Q_T = i_mZ(CA,RA);
double **x = i_mZ(CA,C1);
clrscrn();
printf(" Copy/Past into the octave windows \n\n");
p_Octave_mZ(A,"a",P0,P0);
printf(" [Q, R] = qr (a,0) \n\n");
QR_mZ(A,Q,R);
printf(" Q :");
p_mRZ(Q, S10,P4, C10);
printf(" R :");
p_mRZ(R, S10,P4, C10);
stop();
clrscrn();
ctranspose_mZ(Q,Q_T);
printf(" Q_T :");
pE_mRZ(Q_T,S9,P5, C3);
inv_mZ(R,invR);
printf(" invR :");
pE_mRZ(invR,S9,P5, C6);
stop();
clrscrn();
printf(" Solving this system yields a unique\n"
" least squares solution, namely \n\n");
mul_mZ(invR,Q_T,invR_Q_T);
mul_mZ(invR_Q_T,b,x);
printf(" x = invR * Q_T * b :");
p_mRZ(x,S9,P5 ,C6);
stop();
f_mZ(A);
f_mZ(b);
f_mZ(Q);
f_mZ(Q_T);
f_mZ(R);
f_mZ(invR);
f_mZ(x);
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Network Analysis Using Linear Systems ex 02b.png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
Copy/Past into the octave windows
a=[
+1+0*i,+0+0*i,+0+0*i;
+0+0*i,+1+0*i,+0+0*i;
+0+0*i,-1+0*i,+1+0*i;
-1+0*i,+0+0*i,-1+0*i]
[Q, R] = qr (a,0)
Q :
+0.7071 +0.0000 -0.5000
+0.0000 +0.7071 +0.5000
+0.0000 -0.7071 +0.5000
-0.7071 +0.0000 -0.5000
R :
+1.4142 +0.0000 +0.7071
+0.0000 +1.4142 -0.7071
+0.0000 +0.0000 +1.0000
Press return to continue.
Q_T :
+7.07107e-01 +0.00000e+00 +0.00000e+00
+0.00000e+00 +7.07107e-01 -7.07107e-01
-5.00000e-01 +5.00000e-01 +5.00000e-01
-7.07107e-01
+0.00000e+00
-5.00000e-01
invR :
+7.07107e-01 -0.00000e+00 -5.00000e-01
-0.00000e+00 +7.07107e-01 +5.00000e-01
+0.00000e+00 -0.00000e+00 +1.00000e+00
Press return to continue.
Solving this system yields a unique
least squares solution, namely
x = invR * Q_T * b :
+30.00000
+20.00000
+40.00000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
nqmoko6365k7fdunmwzcv160y1e6hfe
Mathc complexes/05y
0
82580
745848
2025-07-03T08:15:25Z
Xhungab
23827
news
745848
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc complexes (livre)]]
:
[[Mathc complexes/053| '''Application''']]
:
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Analyse de réseau à l'aide de systèmes linéaires (le problème exemple 3a).png|thumb|]]
{{Fichier|c00c.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Save as : c00c.c */
/* ------------------------------------ */
#include "w_a.h"
/* ------------------------------------ */
/* ------------------------------------ */
#define RA R5
#define CA C4
#define Cb C1
/* ------------------------------------ */
/* ------------------------------------ */
int main(void)
{
double ta[RA*(CA*C2+Cb*C2)]={
// x2 x4 x6 x7
-1,+0, +0,+0, +0,+0, +0,+0,
+1,+0, +1,+0, +0,+0, +0,+0,
+0,+0, +0,+0, +1,+0, -1,+0,
+0,+0, +0,+0, +0,+0, +1,+0,
+0,+0, -1,+0, -1,+0, +0,+0,
};
double tb[RA*(CA*C2+Cb*C2)]={
// x2 x4 x6 x7
+20 -50,+0,
+60, +0,
-60, +0,
+90 -20,+0,
+50 -90,+0
};
double **A = ca_A_mZ(ta,i_mZ(RA,CA));
double **b = ca_A_mZ(tb,i_mZ(RA,Cb));
double **Q = i_mZ(RA,CA);
double **Q_T = i_mZ(CA,RA);
double **R = i_mZ(CA,CA);
double **invR = i_mZ(CA,CA);
double **invR_Q_T = i_mZ(CA,RA);
double **x = i_mZ(CA,C1);
clrscrn();
printf(" Copy/Past into the octave windows \n\n");
p_Octave_mZ(A,"a",P0,P0);
printf(" [Q, R] = qr (a,0) \n\n");
QR_mZ(A,Q,R);
printf(" Q :");
p_mRZ(Q, S10,P4, C10);
printf(" R :");
p_mRZ(R, S10,P4, C10);
stop();
clrscrn();
ctranspose_mZ(Q,Q_T);
printf(" Q_T :");
pE_mRZ(Q_T,S9,P5, C3);
inv_mZ(R,invR);
printf(" invR :");
pE_mRZ(invR,S9,P5, C6);
stop();
clrscrn();
printf(" Solving this system yields a unique\n"
" least squares solution, namely \n\n");
mul_mZ(invR,Q_T,invR_Q_T);
mul_mZ(invR_Q_T,b,x);
printf(" x = invR * Q_T * b :");
p_mRZ(x,S9,P5 ,C6);
stop();
f_mZ(A);
f_mZ(b);
f_mZ(Q);
f_mZ(Q_T);
f_mZ(R);
f_mZ(invR);
f_mZ(x);
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Analyse de réseau à l'aide de systèmes linéaires (le problème exemple 3b).png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
Copy/Past into the octave windows
a=[
-1+0*i,+0+0*i,+0+0*i,+0+0*i;
+1+0*i,+1+0*i,+0+0*i,+0+0*i;
+0+0*i,+0+0*i,+1+0*i,-1+0*i;
+0+0*i,+0+0*i,+0+0*i,+1+0*i;
+0+0*i,-1+0*i,-1+0*i,+0+0*i]
[Q, R] = qr (a,0)
Q :
-0.7071 +0.4082 -0.2887 -0.2236
+0.7071 +0.4082 -0.2887 -0.2236
+0.0000 +0.0000 +0.8660 -0.2236
+0.0000 +0.0000 +0.0000 +0.8944
+0.0000 -0.8165 -0.2887 -0.2236
R :
+1.4142 +0.7071 +0.0000 +0.0000
+0.0000 +1.2247 +0.8165 +0.0000
+0.0000 +0.0000 +1.1547 -0.8660
+0.0000 +0.0000 -0.0000 +1.1180
Press return to continue.
Q_T :
-7.07107e-01 +7.07107e-01 +0.00000e+00
+4.08248e-01 +4.08248e-01 +0.00000e+00
-2.88675e-01 -2.88675e-01 +8.66025e-01
-2.23607e-01 -2.23607e-01 -2.23607e-01
+0.00000e+00 +0.00000e+00
+0.00000e+00 -8.16497e-01
+0.00000e+00 -2.88675e-01
+8.94427e-01 -2.23607e-01
invR :
+7.07107e-01 -4.08248e-01 +2.88675e-01 +2.23607e-01
-0.00000e+00 +8.16497e-01 -5.77350e-01 -4.47214e-01
+0.00000e+00 -0.00000e+00 +8.66025e-01 +6.70820e-01
-0.00000e+00 +0.00000e+00 -0.00000e+00 +8.94427e-01
Press return to continue.
Solving this system yields a unique
least squares solution, namely
x = invR * Q_T * b :
+30.00000
+30.00000
+10.00000
+70.00000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
4ltx06d284st4paqyebxfyze8bn2qin
Mathc complexes/05z
0
82581
745850
2025-07-03T08:51:22Z
Xhungab
23827
news
745850
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc complexes (livre)]]
:
[[Mathc complexes/053| '''Application''']]
:
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Network Analysis Using Linear Systems ex 01a.png|thumb|]]
{{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Save as : c00a.c */
/* ------------------------------------ */
#include "w_a.h"
/* ------------------------------------ */
#define RA R4
#define CA C3
#define Cb C1
/* ------------------------------------ */
#define FACTOR_E +1.E-2
/* ------------------------------------ */
/* ------------------------------------ */
int main(void)
{
double ta[RA*(CA*C2+Cb*C2)]={
// x1 x2 x3
+1,+0, +1,+0, +0,+0, // A
+0,+0, -1,+0, -1,+0, // B
+0,+0, +0,+0, +1,+0, // C
-1,+0, +0,+0, +0,+0, // D
};
double tb[RA*(CA*C2+Cb*C2)]={
+50, +0, // A
-40, +0, // B
+20 -10,+0, // C
-30 +10 +0 // D
};
double **A = ca_A_mZ(ta,i_mZ(RA,CA));
double **A_T = ctranspose_mZ(A, i_mZ(CA,RA));
double **b = ca_A_mZ(tb, i_mZ(RA,C1));
double **x = i_mZ(CA,C1);
double **V = i_mZ(CA,CA);
double **V_T = i_mZ(CA,CA);
double **U = i_mZ(RA,CA);
double **U_T = i_mZ(CA,RA);
double **U_TA = i_mZ(CA,CA); // CA,RA RA,CA :CA,CA
double **U_TAV = i_mZ(CA,CA); // CA,CA CA,CA :CA,CA
double **invU_TAV = i_mZ(CA,CA); // :CA,CA
double **V_invU_TAV = i_mZ(CA,CA); // CA,CA CA,CA :CA,CA
double **Pinv = i_mZ(CA,RA);
// Pinv = V_invU_TAV * U_T // CA,CA CA,RA :CA,RA
clrscrn();
printf(" A :");
p_mRZ(A, S10,P2, C6);
printf(" b :");
p_mRZ(b, S10,P2, C6);
stop();
clrscrn();
printf(" U :");
X_U_mZ(A_T,U,FACTOR_E);
p_mRZ(U, S10,P4, C6);
printf(" V :");
X_V_mZ(A_T,V,FACTOR_E);
p_mRZ(V, S10,P4, C6);
ctranspose_mZ(U,U_T);
ctranspose_mZ(V,V_T);
stop();
clrscrn();
printf(" U_TAV :");
mul_mZ(U_T, A, U_TA); // U_TA CA,RA RA,CA :CA,CA
mul_mZ(U_TA, V, U_TAV ); // V :CA,CA
p_mRZ(U_TAV, S11,P4, C6); // U_TAV CA,CA CA,CA :CA,CA
printf(" inv(U_TAV) :");
X_inv_mZ(U_TAV, invU_TAV);
pE_mRZ(invU_TAV, S10,P4, C6);
stop();
clrscrn();
printf(" Pinv = V * inv(U_TAV) * U_T:");
mul_mZ(V, invU_TAV, V_invU_TAV);
mul_mZ(V_invU_TAV, U_T, Pinv);
pE_mRZ(Pinv, S13,P4, C6);
stop();
clrscrn();
printf(" A x = b \n"
" Pinv A x = Pinv b \n"
" Ide x = Pinv b \n\n"
" x = Pinv b ");
mul_mZ(Pinv, b, x);
p_mRZ(x, S12,P4, C6);
stop();
f_mZ(A);
f_mZ(A_T);
f_mZ(b);
f_mZ(x);
f_mZ(V);
f_mZ(V_T);
f_mZ(U);
f_mZ(U_T);
f_mZ(U_TA);
f_mZ(invU_TAV);
f_mZ(V_invU_TAV);
f_mZ(Pinv);
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Network Analysis Using Linear Systems ex 01b.png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
A :
+1.00 +1.00 +0.00
+0.00 -1.00 -1.00
+0.00 +0.00 +1.00
-1.00 +0.00 +0.00
b :
+50.00
-40.00
+10.00
-20.00
Press return to continue.
U :
-0.6533 -0.5000 +0.2706
+0.6533 -0.5000 -0.2706
-0.2706 +0.5000 -0.6533
+0.2706 +0.5000 +0.6533
V :
+0.5000 -0.7071 +0.5000
+0.7071 +0.0000 -0.7071
+0.5000 +0.7071 +0.5000
Press return to continue.
U_TAV :
-1.8478 +0.0000 -0.0000
+0.0000 +1.4142 -0.0000
-0.0000 +0.0000 -0.7654
inv(U_TAV) :
-5.4120e-01 +0.0000e+00 +0.0000e+00
+0.0000e+00 +7.0711e-01 +0.0000e+00
+0.0000e+00 +0.0000e+00 -1.3066e+00
Press return to continue.
Pinv = V * inv(U_TAV) * U_T:
+2.5000e-01 +2.5000e-01 +2.5000e-01 -7.5000e-01
+5.0000e-01 -5.0000e-01 -5.0000e-01 +5.0000e-01
-2.5000e-01 -2.5000e-01 +7.5000e-01 -2.5000e-01
Press return to continue.
A x = b
Pinv A x = Pinv b
Ide x = Pinv b
x = Pinv b
+20.0000
+30.0000
+10.0000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
saw477le89kkswt6z4imn001w183aop
Mathc complexes/060
0
82582
745851
2025-07-03T08:54:18Z
Xhungab
23827
news
745851
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc complexes (livre)]]
:
[[Mathc complexes/053| '''Application''']]
:
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Network Analysis Using Linear Systems ex 02a.png|thumb|]]
{{Fichier|c00b.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Save as : c00b.c */
/* ------------------------------------ */
#include "w_a.h"
/* ------------------------------------ */
#define RA R4
#define CA C3
#define Cb C1
/* ------------------------------------ */
#define FACTOR_E +1.E-2
/* ------------------------------------ */
/* ------------------------------------ */
int main(void)
{
double ta[RA*(CA*C2+Cb*C2)]={
// x1 x3 x4
+1,+0, +0,+0, +0,+0, // A
+0,+0, +1,+0, +0,+0, // B
+0,+0, -1,+0, +1,+0, // C
-1,+0, +0,+0, -1,+0 // D
};
double tb[RA*(CA*C2+Cb*C2)]={
+20+10, +0, // A
+60-10-30,+0, // B
+20, +0, // C
-100+30, +0 // D
};
double **A = ca_A_mZ(ta,i_mZ(RA,CA));
double **A_T = ctranspose_mZ(A, i_mZ(CA,RA));
double **b = ca_A_mZ(tb, i_mZ(RA,C1));
double **x = i_mZ(CA,C1);
double **V = i_mZ(CA,CA);
double **V_T = i_mZ(CA,CA);
double **U = i_mZ(RA,CA);
double **U_T = i_mZ(CA,RA);
double **U_TA = i_mZ(CA,CA); // CA,RA RA,CA :CA,CA
double **U_TAV = i_mZ(CA,CA); // CA,CA CA,CA :CA,CA
double **invU_TAV = i_mZ(CA,CA); // :CA,CA
double **V_invU_TAV = i_mZ(CA,CA); // CA,CA CA,CA :CA,CA
double **Pinv = i_mZ(CA,RA);
// Pinv = V_invU_TAV * U_T // CA,CA CA,RA :CA,RA
clrscrn();
printf(" A :");
p_mRZ(A, S10,P2, C6);
printf(" b :");
p_mRZ(b, S10,P2, C6);
stop();
clrscrn();
printf(" U :");
X_U_mZ(A_T,U,FACTOR_E);
p_mRZ(U, S10,P4, C6);
printf(" V :");
X_V_mZ(A_T,V,FACTOR_E);
p_mRZ(V, S10,P4, C6);
ctranspose_mZ(U,U_T);
ctranspose_mZ(V,V_T);
stop();
clrscrn();
printf(" U_TAV :");
mul_mZ(U_T, A, U_TA); // U_TA CA,RA RA,CA :CA,CA
mul_mZ(U_TA, V, U_TAV ); // V :CA,CA
p_mRZ(U_TAV, S11,P4, C6); // U_TAV CA,CA CA,CA :CA,CA
printf(" inv(U_TAV) :");
X_inv_mZ(U_TAV, invU_TAV);
pE_mRZ(invU_TAV, S10,P4, C6);
stop();
clrscrn();
printf(" Pinv = V * inv(U_TAV) * U_T:");
mul_mZ(V, invU_TAV, V_invU_TAV);
mul_mZ(V_invU_TAV, U_T, Pinv);
pE_mRZ(Pinv, S13,P4, C6);
stop();
clrscrn();
printf(" A x = b \n"
" Pinv A x = Pinv b \n"
" Ide x = Pinv b \n\n"
" x = Pinv b ");
mul_mZ(Pinv, b, x);
p_mRZ(x, S12,P4, C6);
stop();
f_mZ(A);
f_mZ(A_T);
f_mZ(b);
f_mZ(x);
f_mZ(V);
f_mZ(V_T);
f_mZ(U);
f_mZ(U_T);
f_mZ(U_TA);
f_mZ(invU_TAV);
f_mZ(V_invU_TAV);
f_mZ(Pinv);
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Network Analysis Using Linear Systems ex 02b.png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
A :
+1.00 +0.00 +0.00
+0.00 +1.00 +0.00
+0.00 -1.00 +1.00
-1.00 +0.00 -1.00
b :
+30.00
+20.00
+20.00
-70.00
Press return to continue.
U :
-0.2706 -0.5000 +0.6533
+0.2706 -0.5000 -0.6533
-0.6533 +0.5000 -0.2706
+0.6533 +0.5000 +0.2706
V :
+0.5000 +0.7071 -0.5000
-0.5000 +0.7071 +0.5000
+0.7071 +0.0000 +0.7071
Press return to continue.
U_TAV :
-1.8478 -0.0000 -0.0000
-0.0000 -1.4142 -0.0000
-0.0000 -0.0000 -0.7654
inv(U_TAV) :
-5.4120e-01 +0.0000e+00 +0.0000e+00
+0.0000e+00 -7.0711e-01 +0.0000e+00
+0.0000e+00 +0.0000e+00 -1.3066e+00
Press return to continue.
Pinv = V * inv(U_TAV) * U_T:
+7.5000e-01 -2.5000e-01 -2.5000e-01 -2.5000e-01
-2.5000e-01 +7.5000e-01 -2.5000e-01 -2.5000e-01
-5.0000e-01 +5.0000e-01 +5.0000e-01 -5.0000e-01
Press return to continue.
A x = b
Pinv A x = Pinv b
Ide x = Pinv b
x = Pinv b
+30.0000
+20.0000
+40.0000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
qa3fbtg65xii60l6mq6ln99i5cde135
Mathc complexes/061
0
82583
745852
2025-07-03T08:57:04Z
Xhungab
23827
news
745852
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc complexes (livre)]]
:
[[Mathc complexes/053| '''Application''']]
:
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Analyse de réseau à l'aide de systèmes linéaires (le problème exemple 3a).png|thumb|]]
{{Fichier|c00c.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Save as : c00c.c */
/* ------------------------------------ */
#include "w_a.h"
/* ------------------------------------ */
#define RA R5
#define CA C4
#define Cb C1
/* ------------------------------------ */
#define FACTOR_E +1.E-2
/* ------------------------------------ */
/* ------------------------------------ */
int main(void)
{
double ta[RA*(CA*C2+Cb*C2)]={
// x2 x4 x6 x7
-1,+0, +0,+0, +0,+0, +0,+0,
+1,+0, +1,+0, +0,+0, +0,+0,
+0,+0, +0,+0, +1,+0, -1,+0,
+0,+0, +0,+0, +0,+0, +1,+0,
+0,+0, -1,+0, -1,+0, +0,+0,
};
double tb[RA*(CA*C2+Cb*C2)]={
// x2 x4 x6 x7
+20 -50,+0,
+60, +0,
-60, +0,
+90 -20,+0,
+50 -90,+0
};
double **A = ca_A_mZ(ta,i_mZ(RA,CA));
double **A_T = ctranspose_mZ(A, i_mZ(CA,RA));
double **b = ca_A_mZ(tb, i_mZ(RA,C1));
double **x = i_mZ(CA,C1);
double **V = i_mZ(CA,CA);
double **V_T = i_mZ(CA,CA);
double **U = i_mZ(RA,CA);
double **U_T = i_mZ(CA,RA);
double **U_TA = i_mZ(CA,CA); // CA,RA RA,CA :CA,CA
double **U_TAV = i_mZ(CA,CA); // CA,CA CA,CA :CA,CA
double **invU_TAV = i_mZ(CA,CA); // :CA,CA
double **V_invU_TAV = i_mZ(CA,CA); // CA,CA CA,CA :CA,CA
double **Pinv = i_mZ(CA,RA);
// Pinv = V_invU_TAV * U_T // CA,CA CA,RA :CA,RA
clrscrn();
printf(" A :");
p_mRZ(A, S10,P2, C6);
printf(" b :");
p_mRZ(b, S10,P2, C6);
stop();
clrscrn();
printf(" U :");
X_U_mZ(A_T,U,FACTOR_E);
p_mRZ(U, S10,P4, C6);
printf(" V :");
X_V_mZ(A_T,V,FACTOR_E);
p_mRZ(V, S10,P4, C6);
ctranspose_mZ(U,U_T);
ctranspose_mZ(V,V_T);
stop();
clrscrn();
printf(" U_TAV :");
mul_mZ(U_T, A, U_TA); // U_TA CA,RA RA,CA :CA,CA
mul_mZ(U_TA, V, U_TAV ); // V :CA,CA
p_mRZ(U_TAV, S11,P4, C6); // U_TAV CA,CA CA,CA :CA,CA
printf(" inv(U_TAV) :");
X_inv_mZ(U_TAV, invU_TAV);
pE_mRZ(invU_TAV, S10,P4, C6);
stop();
clrscrn();
printf(" Pinv = V * inv(U_TAV) * U_T:");
mul_mZ(V, invU_TAV, V_invU_TAV);
mul_mZ(V_invU_TAV, U_T, Pinv);
pE_mRZ(Pinv, S13,P4, C6);
stop();
clrscrn();
printf(" A x = b \n"
" Pinv A x = Pinv b \n"
" Ide x = Pinv b \n\n"
" x = Pinv b ");
mul_mZ(Pinv, b, x);
p_mRZ(x, S12,P4, C6);
stop();
f_mZ(A);
f_mZ(A_T);
f_mZ(b);
f_mZ(x);
f_mZ(V);
f_mZ(V_T);
f_mZ(U);
f_mZ(U_T);
f_mZ(U_TA);
f_mZ(invU_TAV);
f_mZ(V_invU_TAV);
f_mZ(Pinv);
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Analyse de réseau à l'aide de systèmes linéaires (le problème exemple 3b).png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
A :
-1.00 +0.00 +0.00 +0.00
+1.00 +1.00 +0.00 +0.00
+0.00 +0.00 +1.00 -1.00
+0.00 +0.00 +0.00 +1.00
+0.00 -1.00 -1.00 +0.00
b :
-30.00
+60.00
-60.00
+70.00
-40.00
Press return to continue.
U :
+0.1954 -0.3717 -0.5117 -0.6015
-0.5117 +0.6015 +0.1954 -0.3717
-0.5117 -0.6015 +0.1954 +0.3717
+0.1954 +0.3717 -0.5117 +0.6015
+0.6325 +0.0000 +0.6325 -0.0000
V :
-0.3717 +0.6015 -0.6015 +0.3717
-0.6015 +0.3717 +0.3717 -0.6015
-0.6015 -0.3717 +0.3717 +0.6015
+0.3717 +0.6015 +0.6015 +0.3717
Press return to continue.
U_TAV :
+1.9021 -0.0000 -0.0000 -0.0000
+0.0000 +1.6180 -0.0000 -0.0000
+0.0000 -0.0000 -1.1756 -0.0000
-0.0000 +0.0000 +0.0000 +0.6180
inv(U_TAV) :
+5.2573e-01 +0.0000e+00 +0.0000e+00 +0.0000e+00
+0.0000e+00 +6.1803e-01 +0.0000e+00 +0.0000e+00
+0.0000e+00 +0.0000e+00 -8.5065e-01 +0.0000e+00
+0.0000e+00 +0.0000e+00 +0.0000e+00 +1.6180e+00
Press return to continue.
Pinv = V * inv(U_TAV) * U_T:
-8.0000e-01 +2.0000e-01 +2.0000e-01 +2.0000e-01 +2.0000e-01
+6.0000e-01 +6.0000e-01 -4.0000e-01 -4.0000e-01 -4.0000e-01
-4.0000e-01 -4.0000e-01 +6.0000e-01 +6.0000e-01 -4.0000e-01
-2.0000e-01 -2.0000e-01 -2.0000e-01 +8.0000e-01 -2.0000e-01
Press return to continue.
A x = b
Pinv A x = Pinv b
Ide x = Pinv b
x = Pinv b
+30.0000
+30.0000
+10.0000
+70.0000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
17ncwkywqc1asvnvepdvob6hy8pup1r
Mathc matrices/06g
0
82584
745854
2025-07-03T10:26:59Z
Xhungab
23827
news
745854
wikitext
text/x-wiki
[[Catégorie:Mathc matrices (livre)]]
[[Mathc matrices/a209| '''Application''']]
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Network Analysis Using Linear Systems ex 01a.png|thumb|]]
{{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Save as : c00a.c */
/* ------------------------------------ */
#include "v_a.h"
/* ------------------------------------ */
/* ------------------------------------ */
#define RA R4
#define CA C3
#define Cb C1
/* ------------------------------------ */
/* ------------------------------------ */
int main(void)
{
double ta[RA*(CA+Cb)]={
// x1 x2 x3
+1, +1, +0,// A
+0, -1, -1,// B
+0, +0, +1,// C
-1, +0, +0 // D
};
double tb[RA*(CA+Cb)]={
+50, // A
-40, // B
+20 -10, // C
-30 +10 // D
};
double **A = ca_A_mR(ta,i_mR(RA,CA));
double **b = ca_A_mR(tb,i_mR(RA,Cb));
double **Q = i_mR(RA,CA);
double **Q_T = i_mR(CA,RA);
double **R = i_mR(CA,CA);
double **invR = i_mR(CA,CA);
double **invR_Q_T = i_mR(CA,RA);
double **x = i_mR(CA,C1);
clrscrn();
printf(" Copy/Past into the octave windows \n\n");
p_Octave_mR(A,"a",P0);
printf(" [Q, R] = qr (a,0) \n\n");
QR_mR(A,Q,R);
printf(" Q :");
p_mR(Q, S10,P4, C10);
printf(" R :");
p_mR(R, S10,P4, C10);
stop();
clrscrn();
transpose_mR(Q,Q_T);
printf(" Q_T :");
pE_mR(Q_T,S9,P5, C3);
inv_mR(R,invR);
printf(" invR :");
pE_mR(invR,S9,P5, C6);
stop();
clrscrn();
printf(" Solving this system yields a unique\n"
" least squares solution, namely \n\n");
mul_mR(invR,Q_T,invR_Q_T);
mul_mR(invR_Q_T,b,x);
printf(" x = invR * Q_T * b :");
p_mR(x,S9,P5 ,C6);
stop();
f_mR(A);
f_mR(b);
f_mR(Q);
f_mR(Q_T);
f_mR(R);
f_mR(invR);
f_mR(x);
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Network Analysis Using Linear Systems ex 01b.png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
Copy/Past into the octave windows
a=[
+1,+1,+0;
+0,-1,-1;
+0,+0,+1;
-1,+0,+0]
[Q, R] = qr (a,0)
Q :
+0.7071 +0.4082 -0.2887
+0.0000 -0.8165 -0.2887
+0.0000 +0.0000 +0.8660
-0.7071 +0.4082 -0.2887
R :
+1.4142 +0.7071 +0.0000
+0.0000 +1.2247 +0.8165
+0.0000 +0.0000 +1.1547
Press return to continue.
Q_T :
+7.07107e-01 +0.00000e+00 +0.00000e+00
+4.08248e-01 -8.16497e-01 +0.00000e+00
-2.88675e-01 -2.88675e-01 +8.66025e-01
-7.07107e-01
+4.08248e-01
-2.88675e-01
invR :
+7.07107e-01 -4.08248e-01 +2.88675e-01
-0.00000e+00 +8.16497e-01 -5.77350e-01
-0.00000e+00 -0.00000e+00 +8.66025e-01
Press return to continue.
Solving this system yields a unique
least squares solution, namely
x = invR * Q_T * b :
+20.00000
+30.00000
+10.00000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
7w7x6waw76mtgch7sceifdjt8nb2ys3
Mathc matrices/06h
0
82585
745855
2025-07-03T10:29:04Z
Xhungab
23827
news
745855
wikitext
text/x-wiki
[[Catégorie:Mathc matrices (livre)]]
[[Mathc matrices/a209| '''Application''']]
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Network Analysis Using Linear Systems ex 02a.png|thumb|]]
{{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Save as : c00a.c */
/* ------------------------------------ */
#include "v_a.h"
/* ------------------------------------ */
/* ------------------------------------ */
#define RA R4
#define CA C3
#define Cb C1
/* ------------------------------------ */
/* ------------------------------------ */
int main(void)
{
double ta[RA*(CA+Cb)]={
// x1 x3 x4
+1, +0, +0, // A
+0, +1, +0, // B
+0, -1, +1, // C
-1, +0, -1 // D
};
double tb[RA*(CA+Cb)]={
+20+10, // A
+60 -10 -30, // B
+20, // C
-100+30, // D
};
double **A = ca_A_mR(ta,i_mR(RA,CA));
double **b = ca_A_mR(tb,i_mR(RA,Cb));
double **Q = i_mR(RA,CA);
double **Q_T = i_mR(CA,RA);
double **R = i_mR(CA,CA);
double **invR = i_mR(CA,CA);
double **invR_Q_T = i_mR(CA,RA);
double **x = i_mR(CA,C1);
clrscrn();
printf(" Copy/Past into the octave windows \n\n");
p_Octave_mR(A,"a",P0);
printf(" [Q, R] = qr (a,0) \n\n");
QR_mR(A,Q,R);
printf(" Q :");
p_mR(Q, S10,P4, C10);
printf(" R :");
p_mR(R, S10,P4, C10);
stop();
clrscrn();
transpose_mR(Q,Q_T);
printf(" Q_T :");
pE_mR(Q_T,S9,P5, C3);
inv_mR(R,invR);
printf(" invR :");
pE_mR(invR,S9,P5, C6);
stop();
clrscrn();
printf(" Solving this system yields a unique\n"
" least squares solution, namely \n\n");
mul_mR(invR,Q_T,invR_Q_T);
mul_mR(invR_Q_T,b,x);
printf(" x = invR * Q_T * b :");
p_mR(x,S9,P5 ,C6);
stop();
f_mR(A);
f_mR(b);
f_mR(Q);
f_mR(Q_T);
f_mR(R);
f_mR(invR);
f_mR(x);
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Network Analysis Using Linear Systems ex 02b.png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
Copy/Past into the octave windows
a=[
+1,+0,+0;
+0,+1,+0;
+0,-1,+1;
-1,+0,-1]
[Q, R] = qr (a,0)
Q :
+0.7071 +0.0000 -0.5000
+0.0000 +0.7071 +0.5000
+0.0000 -0.7071 +0.5000
-0.7071 +0.0000 -0.5000
R :
+1.4142 +0.0000 +0.7071
+0.0000 +1.4142 -0.7071
+0.0000 +0.0000 +1.0000
Press return to continue.
Q_T :
+7.07107e-01 +0.00000e+00 +0.00000e+00
+0.00000e+00 +7.07107e-01 -7.07107e-01
-5.00000e-01 +5.00000e-01 +5.00000e-01
-7.07107e-01
+0.00000e+00
-5.00000e-01
invR :
+7.07107e-01 -0.00000e+00 -5.00000e-01
-0.00000e+00 +7.07107e-01 +5.00000e-01
-0.00000e+00 -0.00000e+00 +1.00000e+00
Press return to continue.
Solving this system yields a unique
least squares solution, namely
x = invR * Q_T * b :
+30.00000
+20.00000
+40.00000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
la1xu1f07fwdl1rovk458z1l6v7ei2j
Mathc matrices/06i
0
82586
745856
2025-07-03T10:31:19Z
Xhungab
23827
news
745856
wikitext
text/x-wiki
[[Catégorie:Mathc matrices (livre)]]
[[Mathc matrices/a209| '''Application''']]
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Network Analysis Using Linear Systems ex 03a.png|thumb|]]
{{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Save as : c00a.c */
/* ------------------------------------ */
#include "v_a.h"
/* ------------------------------------ */
/* ------------------------------------ */
#define RA R5
#define CA C4
#define Cb C1
/* ------------------------------------ */
/* ------------------------------------ */
int main(void)
{
double ta[RA*(CA+Cb)]={
// x2 x4 x6 x7
-1, +0, +0, +0,
+1, +1, +0, +0,
+0, +0, +1, -1,
+0, +0, +0, +1,
+0, -1, -1, +0,
};
double tb[RA*(CA+Cb)]={
+20 -50,
+60,
-60,
+90 -20,
+50 -90
};
double **A = ca_A_mR(ta,i_mR(RA,CA));
double **b = ca_A_mR(tb,i_mR(RA,Cb));
double **Q = i_mR(RA,CA);
double **Q_T = i_mR(CA,RA);
double **R = i_mR(CA,CA);
double **invR = i_mR(CA,CA);
double **invR_Q_T = i_mR(CA,RA);
double **x = i_mR(CA,C1);
clrscrn();
printf(" Copy/Past into the octave windows \n\n");
p_Octave_mR(A,"a",P0);
printf(" [Q, R] = qr (a,0) \n\n");
QR_mR(A,Q,R);
printf(" Q :");
p_mR(Q, S10,P4, C10);
printf(" R :");
p_mR(R, S10,P4, C10);
stop();
clrscrn();
transpose_mR(Q,Q_T);
printf(" Q_T :");
pE_mR(Q_T,S9,P5, C3);
inv_mR(R,invR);
printf(" invR :");
pE_mR(invR,S9,P5, C6);
stop();
clrscrn();
printf(" Solving this system yields a unique\n"
" least squares solution, namely \n\n");
mul_mR(invR,Q_T,invR_Q_T);
mul_mR(invR_Q_T,b,x);
printf(" x = invR * Q_T * b :");
p_mR(x,S9,P5 ,C6);
stop();
f_mR(A);
f_mR(b);
f_mR(Q);
f_mR(Q_T);
f_mR(R);
f_mR(invR);
f_mR(x);
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Network Analysis Using Linear Systems ex 03b.png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
Copy/Past into the octave windows
a=[
-1,+0,+0,+0;
+1,+1,+0,+0;
+0,+0,+1,-1;
+0,+0,+0,+1;
+0,-1,-1,+0]
[Q, R] = qr (a,0)
Q :
-0.7071 +0.4082 -0.2887 -0.2236
+0.7071 +0.4082 -0.2887 -0.2236
+0.0000 +0.0000 +0.8660 -0.2236
+0.0000 +0.0000 +0.0000 +0.8944
+0.0000 -0.8165 -0.2887 -0.2236
R :
+1.4142 +0.7071 +0.0000 +0.0000
+0.0000 +1.2247 +0.8165 +0.0000
+0.0000 +0.0000 +1.1547 -0.8660
+0.0000 +0.0000 -0.0000 +1.1180
Press return to continue.
Q_T :
-7.07107e-01 +7.07107e-01 +0.00000e+00
+4.08248e-01 +4.08248e-01 +0.00000e+00
-2.88675e-01 -2.88675e-01 +8.66025e-01
-2.23607e-01 -2.23607e-01 -2.23607e-01
+0.00000e+00 +0.00000e+00
+0.00000e+00 -8.16497e-01
+0.00000e+00 -2.88675e-01
+8.94427e-01 -2.23607e-01
invR :
+7.07107e-01 -4.08248e-01 +2.88675e-01 +2.23607e-01
-0.00000e+00 +8.16497e-01 -5.77350e-01 -4.47214e-01
-0.00000e+00 -0.00000e+00 +8.66025e-01 +6.70820e-01
-0.00000e+00 -0.00000e+00 -0.00000e+00 +8.94427e-01
Press return to continue.
Solving this system yields a unique
least squares solution, namely
x = invR * Q_T * b :
+30.00000
+30.00000
+10.00000
+70.00000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
4rnol5ywk2g47qeel24sxq839424oru
745857
745856
2025-07-03T10:33:24Z
Xhungab
23827
745857
wikitext
text/x-wiki
[[Catégorie:Mathc matrices (livre)]]
[[Mathc matrices/a209| '''Application''']]
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Analyse de réseau à l'aide de systèmes linéaires (le problème exemple 3a).png|thumb|]]
{{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Save as : c00a.c */
/* ------------------------------------ */
#include "v_a.h"
/* ------------------------------------ */
/* ------------------------------------ */
#define RA R5
#define CA C4
#define Cb C1
/* ------------------------------------ */
/* ------------------------------------ */
int main(void)
{
double ta[RA*(CA+Cb)]={
// x2 x4 x6 x7
-1, +0, +0, +0,
+1, +1, +0, +0,
+0, +0, +1, -1,
+0, +0, +0, +1,
+0, -1, -1, +0,
};
double tb[RA*(CA+Cb)]={
+20 -50,
+60,
-60,
+90 -20,
+50 -90
};
double **A = ca_A_mR(ta,i_mR(RA,CA));
double **b = ca_A_mR(tb,i_mR(RA,Cb));
double **Q = i_mR(RA,CA);
double **Q_T = i_mR(CA,RA);
double **R = i_mR(CA,CA);
double **invR = i_mR(CA,CA);
double **invR_Q_T = i_mR(CA,RA);
double **x = i_mR(CA,C1);
clrscrn();
printf(" Copy/Past into the octave windows \n\n");
p_Octave_mR(A,"a",P0);
printf(" [Q, R] = qr (a,0) \n\n");
QR_mR(A,Q,R);
printf(" Q :");
p_mR(Q, S10,P4, C10);
printf(" R :");
p_mR(R, S10,P4, C10);
stop();
clrscrn();
transpose_mR(Q,Q_T);
printf(" Q_T :");
pE_mR(Q_T,S9,P5, C3);
inv_mR(R,invR);
printf(" invR :");
pE_mR(invR,S9,P5, C6);
stop();
clrscrn();
printf(" Solving this system yields a unique\n"
" least squares solution, namely \n\n");
mul_mR(invR,Q_T,invR_Q_T);
mul_mR(invR_Q_T,b,x);
printf(" x = invR * Q_T * b :");
p_mR(x,S9,P5 ,C6);
stop();
f_mR(A);
f_mR(b);
f_mR(Q);
f_mR(Q_T);
f_mR(R);
f_mR(invR);
f_mR(x);
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Analyse de réseau à l'aide de systèmes linéaires (le problème exemple 3b).png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
Copy/Past into the octave windows
a=[
-1,+0,+0,+0;
+1,+1,+0,+0;
+0,+0,+1,-1;
+0,+0,+0,+1;
+0,-1,-1,+0]
[Q, R] = qr (a,0)
Q :
-0.7071 +0.4082 -0.2887 -0.2236
+0.7071 +0.4082 -0.2887 -0.2236
+0.0000 +0.0000 +0.8660 -0.2236
+0.0000 +0.0000 +0.0000 +0.8944
+0.0000 -0.8165 -0.2887 -0.2236
R :
+1.4142 +0.7071 +0.0000 +0.0000
+0.0000 +1.2247 +0.8165 +0.0000
+0.0000 +0.0000 +1.1547 -0.8660
+0.0000 +0.0000 -0.0000 +1.1180
Press return to continue.
Q_T :
-7.07107e-01 +7.07107e-01 +0.00000e+00
+4.08248e-01 +4.08248e-01 +0.00000e+00
-2.88675e-01 -2.88675e-01 +8.66025e-01
-2.23607e-01 -2.23607e-01 -2.23607e-01
+0.00000e+00 +0.00000e+00
+0.00000e+00 -8.16497e-01
+0.00000e+00 -2.88675e-01
+8.94427e-01 -2.23607e-01
invR :
+7.07107e-01 -4.08248e-01 +2.88675e-01 +2.23607e-01
-0.00000e+00 +8.16497e-01 -5.77350e-01 -4.47214e-01
-0.00000e+00 -0.00000e+00 +8.66025e-01 +6.70820e-01
-0.00000e+00 -0.00000e+00 -0.00000e+00 +8.94427e-01
Press return to continue.
Solving this system yields a unique
least squares solution, namely
x = invR * Q_T * b :
+30.00000
+30.00000
+10.00000
+70.00000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
p2chbrjpc7027ie5njc8in9zxw7ei80
Mathc matrices/06j
0
82587
745859
2025-07-03T10:57:06Z
Xhungab
23827
news
745859
wikitext
text/x-wiki
[[Catégorie:Mathc matrices (livre)]]
[[Mathc matrices/a209| '''Application''']]
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Network Analysis Using Linear Systems ex 01a.png|thumb|]]
{{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Save as : c00a.c */
/* ------------------------------------ */
#include "v_a.h"
/* ------------------------------------ */
/* ------------------------------------ */
#define RA R4
#define CA C3
#define Cb C1
/* ------------------------------------ */
#define FACTOR_E +1.E-0
/* ------------------------------------ */
void fun(void)
{
double ta[RA*(CA+Cb)]={
// x1 x2 x3
+1, +1, +0,// A
+0, -1, -1,// B
+0, +0, +1,// C
-1, +0, +0 // D
};
double tb[RA*(CA+Cb)]={
+50, // A
-40, // B
+20 -10, // C
-30 +10 // D
};
double **A = ca_A_mR(ta,i_mR(RA,CA));
double **b = ca_A_mR(tb,i_mR(RA,C1));
double **Pinv = i_mR(CA,RA);
double **x = i_mR(CA,C1);
clrscrn();
printf(" A :");
p_mR(A,S5,P1,C6);
printf(" b :");
p_mR(b,S5,P1,C6);
printf(" Pinv = V * invS_T * U_T ");
Pinv_Rn_mR(A,Pinv,FACTOR_E);
pE_mR(Pinv,S12,P4,C6);
printf(" x = Pinv * b ");
mul_mR(Pinv,b,x);
p_mR(x,S10,P4,C6);
stop();
f_mR(b);
f_mR(A);
f_mR(Pinv);
f_mR(x);
}
/* ------------------------------------ */
int main(void)
{
fun();
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Network Analysis Using Linear Systems ex 01b.png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
A :
+1.0 +1.0 +0.0
+0.0 -1.0 -1.0
+0.0 +0.0 +1.0
-1.0 +0.0 +0.0
b :
+50.0
-40.0
+10.0
-20.0
Pinv = V * invS_T * U_T
+2.5000e-01 +2.5000e-01 +2.5000e-01 -7.5000e-01
+5.0000e-01 -5.0000e-01 -5.0000e-01 +5.0000e-01
-2.5000e-01 -2.5000e-01 +7.5000e-01 -2.5000e-01
x = Pinv * b
+20.0000
+30.0000
+10.0000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
o42hu9pqc4q4bkbqodp1y6l0og0l20o
Mathc matrices/06k
0
82588
745860
2025-07-03T10:59:29Z
Xhungab
23827
news
745860
wikitext
text/x-wiki
[[Catégorie:Mathc matrices (livre)]]
[[Mathc matrices/a209| '''Application''']]
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Network Analysis Using Linear Systems ex 02a.png|thumb|]]
{{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Save as : c00a.c */
/* ------------------------------------ */
#include "v_a.h"
/* ------------------------------------ */
/* ------------------------------------ */
#define RA R4
#define CA C3
#define Cb C1
/* ------------------------------------ */
#define FACTOR_E +1.E-0
/* ------------------------------------ */
void fun(void)
{
double ta[RA*(CA+Cb)]={
// x1 x3 x4
+1, +0, +0, // A
+0, +1, +0, // B
+0, -1, +1, // C
-1, +0, -1 // D
};
double tb[RA*(CA+Cb)]={
+20+10, // A
+60 -10 -30, // B
+20, // C
-100+30, // D
};
double **A = ca_A_mR(ta,i_mR(RA,CA));
double **b = ca_A_mR(tb,i_mR(RA,C1));
double **Pinv = i_mR(CA,RA);
double **x = i_mR(CA,C1);
clrscrn();
printf(" A :");
p_mR(A,S5,P1,C6);
printf(" b :");
p_mR(b,S5,P1,C6);
printf(" Pinv = V * invS_T * U_T ");
Pinv_Rn_mR(A,Pinv,FACTOR_E);
pE_mR(Pinv,S12,P4,C6);
printf(" x = Pinv * b ");
mul_mR(Pinv,b,x);
p_mR(x,S10,P4,C6);
stop();
f_mR(b);
f_mR(A);
f_mR(Pinv);
f_mR(x);
}
/* ------------------------------------ */
int main(void)
{
fun();
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Network Analysis Using Linear Systems ex 02b.png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
A :
+1.0 +0.0 +0.0
+0.0 +1.0 +0.0
+0.0 -1.0 +1.0
-1.0 +0.0 -1.0
b :
+30.0
+20.0
+20.0
-70.0
Pinv = V * invS_T * U_T
+7.5000e-01 -2.5000e-01 -2.5000e-01 -2.5000e-01
-2.5000e-01 +7.5000e-01 -2.5000e-01 -2.5000e-01
-5.0000e-01 +5.0000e-01 +5.0000e-01 -5.0000e-01
x = Pinv * b
+30.0000
+20.0000
+40.0000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
sb259ws2vkdvnwjd20sfv1x1ievatb8
Mathc matrices/06l
0
82589
745861
2025-07-03T11:01:46Z
Xhungab
23827
news
745861
wikitext
text/x-wiki
[[Catégorie:Mathc matrices (livre)]]
[[Mathc matrices/a209| '''Application''']]
Installer et compiler ces fichiers dans votre répertoire de travail.
[[File:Analyse de réseau à l'aide de systèmes linéaires (le problème exemple 3a).png|thumb|]]
{{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ------------------------------------ */
/* Save as : c00a.c */
/* ------------------------------------ */
#include "v_a.h"
/* ------------------------------------ */
/* ------------------------------------ */
#define RA R5
#define CA C4
#define Cb C1
/* ------------------------------------ */
#define FACTOR_E +1.E-0
/* ------------------------------------ */
void fun(void)
{
double ta[RA*(CA+Cb)]={
// x2 x4 x6 x7
-1, +0, +0, +0,
+1, +1, +0, +0,
+0, +0, +1, -1,
+0, +0, +0, +1,
+0, -1, -1, +0,
};
double tb[RA*(CA+Cb)]={
+20 -50,
+60,
-60,
+90 -20,
+50 -90
};
double **A = ca_A_mR(ta,i_mR(RA,CA));
double **b = ca_A_mR(tb,i_mR(RA,C1));
double **Pinv = i_mR(CA,RA);
double **x = i_mR(CA,C1);
clrscrn();
printf(" A :");
p_mR(A,S5,P1,C6);
printf(" b :");
p_mR(b,S5,P1,C6);
stop();
clrscrn();
printf(" Pinv = V * invS_T * U_T ");
Pinv_Rn_mR(A,Pinv,FACTOR_E);
pE_mR(Pinv,S12,P4,C6);
printf(" x = Pinv * b ");
mul_mR(Pinv,b,x);
p_mR(x,S10,P4,C6);
stop();
f_mR(b);
f_mR(A);
f_mR(Pinv);
f_mR(x);
}
/* ------------------------------------ */
int main(void)
{
fun();
return 0;
}
/* ------------------------------------ */
/* ------------------------------------ */
</syntaxhighlight>
[[File:Analyse de réseau à l'aide de systèmes linéaires (le problème exemple 3b).png|thumb|]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
A :
-1.0 +0.0 +0.0 +0.0
+1.0 +1.0 +0.0 +0.0
+0.0 +0.0 +1.0 -1.0
+0.0 +0.0 +0.0 +1.0
+0.0 -1.0 -1.0 +0.0
b :
-30.0
+60.0
-60.0
+70.0
-40.0
Press return to continue.
Pinv = V * invS_T * U_T
-8.0000e-01 +2.0000e-01 +2.0000e-01 +2.0000e-01 +2.0000e-01
+6.0000e-01 +6.0000e-01 -4.0000e-01 -4.0000e-01 -4.0000e-01
-4.0000e-01 -4.0000e-01 +6.0000e-01 +6.0000e-01 -4.0000e-01
-2.0000e-01 -2.0000e-01 -2.0000e-01 +8.0000e-01 -2.0000e-01
x = Pinv * b
+30.0000
+30.0000
+10.0000
+70.0000
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
8jf6z5tx9xwnew636k2gbt0ncrg70h2